Search code examples
swiftgenericsswift-protocolsassociated-types

Use associated type protocol as a return type of a generic function


Protocols with associated types is confusing:

// Lets say I have two possible type of responses
struct OtpResponse {}
struct SsoResponse {}

// A simple protocol to mandate the return of token from respective concrete type
protocol AuthenticationProvider {
    associatedtype ResponseType
    func getToken(completion: @escaping (ResponseType?, NSError?) -> Void)
}

// A type of auth provider
struct OtpBasedAuthProvider:AuthenticationProvider {
    typealias ResponseType = OtpResponse
    func getToken(completion: @escaping (OtpResponse?, NSError?) -> Void) {
        let otpResponse = OtpResponse()
        completion(otpResponse, nil)
    }
}

// Another type of auth provider
struct SsoBasedAuthProvider: AuthenticationProvider {
    typealias ResponseType = SsoResponse
    func getToken(completion: @escaping (SsoResponse?, NSError?) -> Void) {
        let ssoResponse = SsoResponse()
        completion(ssoResponse, nil)
    }
}

// There is some external logic to decide which type of auth provider to be used
func getProviderTypeFromSomeLogicOtherLogic() -> Int{
    return 1 // simply for dummy
}

// Factory to return a concrete implementaton of auth provider
class AuthProviderFactory {
    func getAuthProvider<T: AuthenticationProvider>(type:Int) -> T {
        if type == 1 {
            return SsoBasedAuthProvider() as! T
        }
        else {
            return OtpBasedAuthProvider() as! T
        }
    }
}

Now to use the code above, I want to do something like this:

func executeNetworkCall() -> Void {
    let factory = AuthProviderFactory() // 1
    let authProvider = factory.getAuthProvider(type:  getProviderTypeFromSomeLogicOtherLogic()) // 2
    authProvider.getToken{ (resp, error) in // 3
        // some code
    }
}

In the above, line number 2 where I am trying to get provider type from factory is giving me error as :

Generic parameter 'T' could not be inferred.

I know I can get rid of compilation error by doing something like this :

let authProvider:SsoBasedAuthProvider = factory.getAuthProvider(type: getProviderTypeFromSomeLogicOtherLogic())

But thats not the point, I dont know which provider will be returned and I want to call .getToken from that provider.


Solution

  • Protocols with associatedtype can’t be used in form of composition, which is a drawback and definitely irritating sometimes. But, you can create your own Type Erasure class to make this work.

    You can study more about type erasure from this link: https://www.donnywals.com/understanding-type-erasure-in-swift/. You can find many more on Google.

    This is how Apple has implemented it internally, by making few changes we can make it work our way.

    Below is the code I came up with:

     //Let's say I have two possible type of responses
    struct OtpResponse{}
    struct SsoResponse{}
    
    //A simple protocol to mandate the return of token from respective concrete type
    protocol AuthenticationProvider{
        associatedtype ResponseType
        func getToken(completion: @escaping(ResponseType?, NSError?) -> Void)
    }
    
    //A type of auth provider
    struct OtpBasedAuthProvider:AuthenticationProvider{
        
        func getToken(completion: @escaping (OtpResponse?, NSError?) -> Void) {
            let otpResponse = OtpResponse()
            completion(otpResponse,nil)
        }
    }
    
    //Another type of auth provider
    struct SsoBasedAuthProvider:AuthenticationProvider{
        
        func getToken(completion: @escaping (SsoResponse?, NSError?) -> Void) {
            let ssoResponse = SsoResponse()
            completion(ssoResponse,nil)
        }
    }
    
    // there is some external logic to decide which type of auth provider to be used
    func getProviderTypeFromSomeLogicOtherLogic() -> Int{
        return 1//simply for dummy
    }
    

    Type Erasure:

    class _AnyCacheBox<Storage>:AuthenticationProvider{
            func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
                fatalError("Never to be called")
            }
            
        }
        
        final class _CacheBox<C:AuthenticationProvider>: _AnyCacheBox<C.ResponseType>{
            private var _base:C
            
            init(base:C) {
                self._base = base
            }
            
            override func getToken(completion: @escaping (C.ResponseType?, NSError?) -> Void) {
                _base.getToken(completion: completion)
            }
        }
        
        struct AnyCache<Storage>:AuthenticationProvider{
            private let _box: _AnyCacheBox<Storage>
            
            init<C:AuthenticationProvider>(cache:C) where C.ResponseType == Storage {
                _box = _CacheBox(base: cache)
            }
            
            func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
                _box.getToken(completion: completion)
            }
        }
        
        
        //Factory to return a concrete implementaton of auth provider
        class AuthProviderFactory{
            func getOTPAuthProvider() -> AnyCache<OtpResponse>{
                
                let obj : AnyCache = AnyCache(cache: OtpBasedAuthProvider())
                return obj
                
            }
            
            func getSSoAuthProvider() -> AnyCache<SsoResponse>{
                let obj : AnyCache = AnyCache(cache: SsoBasedAuthProvider())
                return obj
            }
        }
    

    Below is how client can invoke methods in Factory-:

    func executeNetworkCall() -> Void{
            let factory = AuthProviderFactory()
            let authProvider = factory.getOTPAuthProvider()
            authProvider.getToken{(resp,error) in
                //some code
                print(resp)
            }
        }
    

    It’s a bit involving and could take time to understand.