The code below works just fine:
protocol VariableType {
associatedtype T
var value : T { get }
}
class UserDefaultsVariable<T> : VariableType {
let storageKey : String
var value : T {
get {
return UserDefaults.standard.object(forKey: storageKey) as! T
}
set(value) {
UserDefaults.standard.set(value, forKey: storageKey)
}
}
init( storageKey : String, initialValue : T ) {
self.storageKey = storageKey
// I want dynamic dispatch here so that if T: RawRepresentable then the
// function in the extension is called
self.registerDefaultValue(initialValue: initialValue)
}
func registerDefaultValue( initialValue : T ) {
debugPrint("this is called both times!")
UserDefaults.standard.register(defaults: [storageKey : initialValue])
}
}
Calling:
let simpleDefault = UserDefaultsVariable<Int>(storageKey: "simple", initialValue: 0)
let initialValue = simpleDefault.value
results in the initial value being 0 as expected.
The problem arises when I try to extend this to support RawRepresentable
types:
extension UserDefaultsVariable where T: RawRepresentable {
var value: T {
get {
let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
return T(rawValue: rawValue)!
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
}
}
func registerDefaultValue( initialValue : T ) {
debugPrint("this is never called!")
UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
}
}
When I call this:
let simpleDefault = UserDefaultsVariable<Int>(storageKey: "simple", initialValue: 0)
let initialValue = simpleDefault.value
enum Direction : Int {
case left = 0
case right = 1
}
let enumedDefault = UserDefaultsVariable<Direction>(storageKey: "enumed", initialValue: .left)
the code crashes because the registerDefaults implementation in the UserDefaultsVariable<T>
is called instead of the specialized UserDefaultsVariable<T:RawRepresentable>
implementation.
Making the call from outside the initializer, e.g.
enumedDefault.registerDefaultValue(initialValue: .left)
calls the correct implementation!? So it looks like the dispatch is usually dynamic, but not inside of an initializer? or possibly the entire class?
Any help would be very much appreciated.
Solution
As @matt points out, I wrongly expect Swift to call the constrained version of the function via some form of polymorphism, but the compiler resolves the call at compile time and makes no attempt at finding "the most specific implementation"..
@Asperi presents a workable solution, but with the disadvantage that the DefaultsVariable can be initialized without an initial value, which is something I was trying to avoid.
What I ended up doing was to introduce polymorphism by simply subclassing UserDefaultsVariable:
class RawRepresentableUserDefaultsVariable<T: RawRepresentable> : UserDefaultsVariable<T>{
override var value: T {
get {
let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
return T(rawValue: rawValue)!
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
}
}
override func registerDefaultValue( initialValue : T ) {
UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
}
}
let enumedDefault = RawRepresentableUserDefaultsVariable<Direction>(storageKey: "enumed", initialValue: .left)
let initialEnumValue = enumedDefault.value
This compiles fine and calls the registerDefaultValue
in the subclass from the initializer in the superclass.
I really appreciate the help.
Here is possible approach.
The idea is to make initializer convenience in extension, so generics specialised extension overlaps default one.
Tested & works with Xcode 11.4 / swift 5.2
class UserDefaultsVariable<T> : VariableType {
let storageKey : String
init(storageKey: String) {
self.storageKey = storageKey
}
var value : T {
get {
return UserDefaults.standard.object(forKey: storageKey) as! T
}
set(value) {
UserDefaults.standard.set(value, forKey: storageKey)
}
}
func registerDefaultValue( initialValue : T ) {
UserDefaults.standard.register(defaults: [storageKey : initialValue])
}
}
extension UserDefaultsVariable {
// this initializer uses default T
convenience init(storageKey : String, initialValue : T) {
self.init(storageKey: storageKey)
self.registerDefaultValue(initialValue: initialValue)
}
}
extension UserDefaultsVariable where T: RawRepresentable {
// this initializer uses specialised T
convenience init(storageKey : String, initialValue : T) {
self.init(storageKey: storageKey)
self.registerDefaultValue(initialValue: initialValue)
}
var value: T {
get {
let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
return T(rawValue: rawValue)!
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
}
}
func registerDefaultValue( initialValue : T ) {
UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
}
}