Search code examples
swiftgenericsinstantiationswift-protocols

Creating a new instance of a class with a protocol-conforming class as an argument


I have the following code setup (written in Swift):

protocol DataFetcherType {
    init(_ mainData: String, fetchData: Bool)
}

class DataFetcher1: DataFetcherType {
    required init(_ mainData: String, fetchData: Bool) {
    }
}

class DataFetcher2: DataFetcherType {
    required init(_ mainData: String, fetchData: Bool) {
    }
}

struct Data<FetcherType: DataFetcherType> {
    var mainData: String
    var fetcher: DataFetcherType
    
    init(_ mainData: String, using fetcher: FetcherType, fetchData: Bool = true) {
        self.mainData = mainData
        self.fetcher = FetcherType(mainData, fetchData: fetchData)
    }
}

And I'm trying to instantiate Data like so:

Data("foo", using: DataFetcher1)

But I get the error:

Type 'DataFetcher1.Type' cannot conform to 'DataFetcherType'

The same is true if I try it with DataFetcher2.

I've been at it for hours and I feel like I've changed every possible thing, but I just can't get it to work. My initial attempt didn't use Generics, but I eventually decided that might be what I need to do. Was that wrong, or am I fundamentally missing something about how Swift works?


Solution

  • You don't need to pass any type at all. The using parameter is completely unnecessary. Delete it. This is a generic; passing the type makes the generic pointless!

    Just resolve the generic when you declare what you want to instantiate. (And do not call something Data, that name is taken.)

    So, if we start with this:

    protocol DataFetcherType {
        init(_ mainData: String, fetchData: Bool)
    }
    class DataFetcher1: DataFetcherType {
        required init(_ mainData: String, fetchData: Bool) {
        }
    }
    class DataFetcher2: DataFetcherType {
        required init(_ mainData: String, fetchData: Bool) {
        }
    }
    

    ... then this is all you need:

    struct MyData<FetcherType: DataFetcherType> {
        var mainData: String
        var fetcher: FetcherType
        init(_ mainData: String, fetchData: Bool = true) {
            self.mainData = mainData
            self.fetcher = FetcherType.init(mainData, fetchData: fetchData)
        }
    }
    

    ...because to make one, you can just resolve the type:

        let mydata = MyData<DataFetcher1>("hey")
    

    Also, notice that I have changed the type of the variable fetcher. It should be the generic type, not the protocol.

    In general if you find yourself passing types around in Swift, that's a bad smell and you should rethink. It can be done, and it sometimes is done, but on the whole Swift does not like metatypes and you should avoid them.

    And the same is true of protocols; if you find you are typing something as a protocol, rethink.

    This is exactly why generics exist, so you don't do those things.