Problem Summary
I have a generic view subclass TintableView<T>: UIView
, which implements a protocol TintStateComputing
with identical associated type T
. TintStateComputing
's constrained extension implementation is not being called; its unconstrained extension implementation is called instead.
The TintStateComputing
protocol has a function computeTintState() -> T
, which does what it sounds like: checks the local state properties, and returns the according instance of T
.
I want to write extension implementations of func computeTintState() -> T
on TintStateComputing
, constrained on the type of T
. For example, with a ControlState
enum:
extension TintStateComputing where T == ControlState {
func computeTintState() -> T {
return self.isEnabled ? .enabled : T.default
}
}
However, in order to complete the protocol conformance, I think I need to account for other values of T
. So I've also stated an unconstrained extension to TintStateComputing
. This unconstrained extension implementation is always being called, instead of the constrained implementation.
extension TintStateComputing {
func computeTintState() -> T {
return _tintState ?? T.default
}
}
Playground Testbed
import UIKit
// MARK: - Types
public enum ControlState: Int {
case normal, enabled, highlighted, selected, disabled
}
public protocol Defaultable {
static var `default`: Self { get }
}
extension ControlState: Defaultable {
public static let `default`: ControlState = .normal
}
// MARK: - TintStateComputing declaration
public protocol TintStateComputing {
associatedtype TintState: Hashable & Defaultable
func computeTintState() -> TintState
var _tintState: TintState? { get }
var isEnabled: Bool { get }
}
// MARK: - TintableView declaration
class TintableView<T: Hashable & Defaultable>: UIView, TintStateComputing {
// `typealias TintState = T` is implictly supplied by compiler
var _tintState: T?
var isEnabled: Bool = true { didSet { _tintState = nil }}
var tintState: T {
get {
guard _tintState == nil else {
return _tintState!
}
return computeTintState()
}
set {
_tintState = newValue
}
}
}
// MARK: - Unconstrained TintStateComputing extension
extension TintStateComputing {
func computeTintState() -> TintState {
return _tintState ?? TintState.default
}
}
// MARK: - Constrained TintStateComputing extension
extension TintStateComputing where TintState == ControlState {
func computeTintState() -> TintState {
return self.isEnabled ? .enabled : TintState.default
}
}
// MARK: - Test Case
let a = TintableView<ControlState>()
a.isEnabled = true
print("Computed tint state: \(a.tintState); should be .enabled") // .normal
print("finished")
Workaround
I realized this morning that since (at least for now) what I'm really trying to accomplish is handle the isEnabled: Bool
flag on the view, I could follow the same pattern as used for Defaultable
to define a default 'enabled' case.
public protocol Enableable {
static var defaultEnabled: Self { get }
}
extension ControlState: Defaultable, Enableable {
public static let `default`: ControlState = .normal
public static let defaultEnabled: ControlState = .enabled
}
At that point, I can really eliminate the TintStateComputing
protocol, and update my view's tintState: T
implementation to account for the flag directly.
var tintState: T {
get {
guard _tintState == nil else { return _tintState! }
return self.isEnabled ? T.defaultEnabled : T.default
}
set {
_tintState = newValue
}
}
It's not as generalized as putting the implementation in a constrained extension, but it will work for now. I think that if I have future subclasses with multi-dimensional tint-state types (e.g. 'enabled' + 'in-range') I'll be able to address via override
.
struct CalendarState: Equatable, Hashable, Defaultable, Enableable {
let x: Int
static let `default`: CalendarState = CalendarState(x: 0)
static let defaultEnabled: CalendarState = CalendarState(x: 1)
}
class ControlTintableView: TintableView<ControlState> {}
class CalendarView: TintableView<CalendarState> {}
let a = ControlTintableView()
a.isEnabled = true
print("ControlTintableView computed tint state: \(a.tintState); should be: .enabled") // .enabled
let b = CalendarView()
b.isEnabled = true
print("CalendarView computed tint state: \(b.tintState); should be: CalendarState(x: 1)") // CalendarState(x: 1)