Search code examples
swiftgenericsprotocols

How to handle multiple generic protocols in Swift?


I'm trying to use two generic protocols that relate to each other as:

protocol PersistableData {}

protocol DataStore: class {
    associatedtype DataType: PersistableData

    func save(data: DataType, with key: String)

    func retreive(from key: String) -> DataType?
}

protocol PersistentDataModel {
    // Swift infers that DataType: PersistableData as DataType == DataStoreType.DataType: PersistableData
    // Setting it explicitly makes the compiler fail
    associatedtype DataType
    associatedtype DataStoreType: DataStore where DataStoreType.DataType == DataType
}

extension String: PersistableData {}

protocol StringDataStore: DataStore {
    associatedtype DataType = String
}


class Test: PersistentDataModel {
    typealias DataType = String
    typealias DataStoreType = StringDataStore
}

However Xcode fails to compile saying that Type 'Test' does not conform to protocol 'PersistentDataModel' and suggesting that Possibly intended match 'DataStoreType' (aka 'StringDataStore') does not conform to 'DataStore' while StringDataStore is defined as conforming to DataStore

I've read a few good resources about generic protocols including SO and this Medium post, but I could not find where the issue is.


Solution

  • This happening because your typealias for associatedtype should have concretion, not abstraction.

    Thus, for your case, StringDataStore should be a class, not protocol.

    protocol PersistableData {}
    
    protocol DataStore: class {
    associatedtype DataType: PersistableData
    
        func save(data: DataType, with key: String)
    
        func retreive(from key: String) -> DataType?
    }
    
    protocol PersistentDataModel {
        // Swift infers that DataType: PersistableData as DataType == DataStoreType.DataType: PersistableData
        // Setting it explicitly makes the compiler fail
        associatedtype DataType
        associatedtype DataStoreType: DataStore where DataStoreType.DataType == DataType
    }
    extension String: PersistableData {}
    
    class StringDataStore: DataStore {
        typealias DataType = String
    
        func save(data: String, with key: String) {
            //
        }
    
        func retreive(from key: String) -> String? {
            return nil
        }
    }
    
    class Test: PersistentDataModel {
        typealias DataType = String
        typealias DataStoreType = StringDataStore
    }
    

    However, you can keep using protocols and solve it by using additional generics conditions in your Test class:

    class Test<T: StringDataStore>: PersistentDataModel where T.DataType == String {
        typealias DataStoreType = T
        typealias DataType = T.DataType
    }
    

    Using this you can tell to compiler that concrete type will be passed to Test somewhere else.

    Like this:

    class ConcreteStringDataStore: StringDataStore {
        func save(data: String, with key: String) {
            //
        }
    
        func retreive(from key: String) -> String? {
            return nil
        }
    }
    
    let test = Test<ConcreteStringDataStore>()