Search code examples
swiftgenericsswift-protocolsswift-extensions

Swift protocol with generic types


I'm trying to create a protocol that has a static method that returns a generic type. In most cases what I have seems to work reasonably well. The challenge comes in when I want to use an extension to return this generic value. Here's what I have. This code can be placed into a playground.

Here's the first protocol I want which contains the associatedtype

protocol AWSerializable {
    associatedtype T

    static func deserialize(dictionary: [String : Any]) -> T?
    func serialize() -> [String : Any]
}

Then I created another protocol which allows me to create instances that do the action of deserializing:

protocol AWDeserializer {
    func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T?
    func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]?
}

Here is an example that happily implements the AWSerializable protocol:

class FooBar: AWSerializable {

    typealias T = FooBar

    var foo = ""
    var bar = ""

    static func deserialize(dictionary: [String : Any]) -> FooBar? {
        let fooBar = FooBar()

        fooBar.foo = (dictionary["foo"] as? String) ?? ""
        fooBar.bar = (dictionary["bar"] as? String) ?? ""

        return fooBar
    }

    func serialize() -> [String : Any] {
        var serialized = [String : Any]()

        serialized["foo"] = foo
        serialized["bar"] = bar

        return serialized
    }

}

So far so good. The challenge comes in when I want to create an extension on UserDefaults to implement the AWDeserializer protocol.

extension UserDefaults: AWDeserializer {
    func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T? {
        if let serialized = UserDefaults.standard.object(forKey: key) as? [String : Any] {
            return T.deserialize(dictionary: serialized)
        }

        return nil
    }

    func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]? {
        if let data = UserDefaults.standard.array(forKey: key) as? [[String : Any]] {
            var values = [T]()

            for entry in data {
                if let value = T.deserialize(dictionary: entry) {
                    values.append(value)
                }
            }

            return values
        }

        return nil
    }
}

The problem here is with T.deserialize(dictionary: serialized). I get the following error:

enter image description here

This is easily fixed by applying the suggested solution, or preferably by changing the line to return T.deserialize(dictionary: serialized) as? T

But I don't like the fact that this optional cast is required to begin with. Is there a way to define the protocols that doesn't require this cast?


Solution

  • Maybe it's much better to use this protocol AWDeserializer:

    protocol AWDeserializer {
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T? where T.T == T
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]? where T.T == T
    }
    

    Instead of:

    protocol AWDeserializer {
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T?
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]?
    }
    

    And here is the rest of code:

    protocol AWSerializable {
        associatedtype T
    
        static func deserialize(dictionary: [String : Any]) -> T?
        func serialize() -> [String : Any]
    }
    
    protocol AWDeserializer {
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T? where T.T == T
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]? where T.T == T
    }
    
    class FooBar: AWSerializable {
    
        typealias T = FooBar
    
        var foo = ""
        var bar = ""
    
        static func deserialize(dictionary: [String : Any]) -> FooBar? {
            let fooBar = FooBar()
    
            fooBar.foo = (dictionary["foo"] as? String) ?? ""
            fooBar.bar = (dictionary["bar"] as? String) ?? ""
    
            return fooBar
        }
    
        func serialize() -> [String : Any] {
            var serialized = [String : Any]()
    
            serialized["foo"] = foo
            serialized["bar"] = bar
    
            return serialized
        }
    
    }
    
    extension UserDefaults: AWDeserializer {
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> T? where T.T == T {
            if let serialized = UserDefaults.standard.object(forKey: key) as? [String : Any] {
                return T.deserialize(dictionary: serialized)
            }
    
            return nil
        }
    
        func deserializeWithKey<T: AWSerializable>(_ key: String!) -> [T]? where T.T == T  {
            if let data = UserDefaults.standard.array(forKey: key) as? [[String : Any]] {
                var values = [T]()
    
                for entry in data {
                    if let value = T.deserialize(dictionary: entry) {
                        values.append(value)
                    }
                }
    
                return values
            }
    
            return nil
        }
    }