Search code examples
swiftgenericsprotocols

Testing for compliance with and casting to RawRepresentable protocol


I have some generic code that allows me to read and write various types to the defaults system, e.g. value getters and setters:

var value : T {
    
    get {
        if T.self == Int.self {
            return UserDefaults.standard.integer(forKey: storageKey) as! T
        } else if T.self == Double.self {
            return UserDefaults.standard.double(forKey: storageKey) as! T
        } else if T.self == Float.self {
            return UserDefaults.standard.float(forKey: storageKey) as! T
        } else if T.self == Bool.self {
            return UserDefaults.standard.bool(forKey: storageKey) as! T
        } else if T.self == String.self {
            return UserDefaults.standard.string(forKey: storageKey) as! T
        } else {
            return UserDefaults.standard.value(forKey: self.storageKey) as! T
        }
    }
    
    set(value) {
        UserDefaults.standard.set(value, forKey: storageKey)
        UserDefaults.standard.synchronize()
    }
}

Now I want to add my own enum types to this mechanism by making them RawRepresentable<Int>, e.g.

enum Direction : Int, RawRepresentable {
    case left = 0
    case right = 1
}

Unfortunately, I can neither find the magic incantation to test whether T conforms to the RawRepresentable protocol, nor can I cast T to the RawRepresentable protocol, because no matter what I try, I always end up with a Protocol 'RawRepresentable' can only be used as a generic constraint because it has Self or associated type requirements.

I have tried every where and as incantation until I have started doubting that it can be done at all!?

I'm in Swift 5 and the goal is to create new instance by invoking CustomType(rawValue:) and getting the Int value by calling myValue.rawValue.


Solution

  • As @vadian said, all those type checks can be replaced be a single call to UserDefaults.standard.object() and conditional casting. Also the type of the value property needs to be an optional to handle the case where the property is not set (or not of the correct type):

    struct DefaultKey<T> {
        let storageKey: String
        
        var value: T? {
            get {
                return UserDefaults.standard.object(forKey: storageKey) as? T
            }
            nonmutating set {
                UserDefaults.standard.set(newValue, forKey: storageKey)
            }
        }
    }
    

    And then you can define a constrained extension method where you specialize the computed property for the case of RawRepresentable types:

    extension DefaultKey where T: RawRepresentable {
        var value: T? {
            get {
                if let rawValue = UserDefaults.standard.object(forKey: storageKey) as? T.RawValue {
                    return T(rawValue: rawValue)
                }
                return nil
            }
            nonmutating set {
                UserDefaults.standard.set(newValue?.rawValue, forKey: storageKey)
            }
        }
    }
    

    Example usage:

    enum Direction : Int {
        case left = 0
        case right = 1
    }
    
    let key1 = DefaultKey<Int>(storageKey: "foo")
    key1.value = 123
    let key2 = DefaultKey<Direction>(storageKey: "bar")
    key2.value = .right
    
    print(key1.value as Any) // Optional(123)
    print(key2.value as Any) // Optional(Direction.right)
    

    Note that this can still crash if used with non-property-list types. To be on the safe side, you would have to restrict the extensions to types which are known to be user defaults storable (integers, floats, strings, ...):

    protocol UserDefaultsStorable {}
    extension Int: UserDefaultsStorable {}
    extension Float: UserDefaultsStorable {}
    // ...
    
    struct DefaultKey<T> {
            let storageKey: String
    }
    
    extension DefaultKey where T: UserDefaultsStorable { .. }
    
    extension DefaultKey where T: RawRepresentable, T.RawValue: UserDefaultsStorable { ... }