Search code examples
iosswiftgenericsdependency-injectionassociated-types

Dependency injection with associated types causing arguments without type names (undescores) in Swift


The situation is following: I'm using a protocol to inject dependencies and the best way I found to implement this in Swift is to use the associatedtype keyword. I am also using protocol composition since some implementations of TestProtocol need more than one dependency.

protocol TestProtocol: class {
    associatedtype Dependencies
    func inject(_ dependency: Dependencies)
}

protocol HasSomething {
    var something: Something { get set }
}

protocol HasSomethingElse {
    var somethingElse: SomethingElse { get set }
}

To use this I found that I'll need to use generics like this:

class TestService<T> where T: TestProtocol, T.Dependencies == TestService {

    weak var testProtocol: T?

    init(with testProtocol: T) {
        self.testProtocol = testProtocol        
        self.testProtocol?.inject(self)
    }
}

Now when I want to use this service somewhere else and I'm trying to initiate it I get following problem:

problem with generics

The parameter is displayed as _ and not as the protocol name TestProtocol.

Let's say I would use this code in a library. How would a user know (without reading the documentation of course) what type could be used in this context when he is not even knowing what protocol he has to implement?

Is there a better way on how to use dependency injection with the type actually being displayed to the user, or am I doing something wrong in the where clause of the TestService class, or is this simply not possible in the current versions of Swift?


Solution

  • There is nothing wrong with your code, this is simply not possible.

    class TestService<T> where T: TestProtocol
    

    The where clause means T could be anything, with the constraint that the given object must conform to TestProtocol.

    The Xcode autocomplete feature only displays the resolved type when available, but it doesn't show the constraints on a generic, and unfortunately there is nothing you can do about that.

    You have the exact same issue in the swift standard library, with Dictionary for example

    public struct Dictionary<Key, Value> where Key : Hashable {
    
      public init(dictionaryLiteral elements: (Key, Value)...) {
        // ..
      }
    }
    

    The generic Key as a constraint to Hashable, but Xcode still shows _ in the autocomplete list.

    Dictionary<_, _> (dictionaryLiteral: (_, _)...)

    I guess Swift developers are use to this behaviour, so it won't be a big issue, even if your code is embedded in a library.

    How would a user know (without reading the documentation of course) what type could be used in this context when he is not even knowing what protocol he has to implement?

    Because Xcode is pretty clear about the protocol requirement. If I try to initialize the TestService with a String I'll get the error:

    Referencing initializer 'init(with:)' on 'TestService' requires that 'String' conform to 'TestProtocol'
    

    Which is pretty self explanatory.