I had a bit of an issue and partly got to the bottom of it. I thought it might be an interesting discussion anyway.
I have the code below which has two versions of a method on a class, one is intended to be called in production code, the other version is intended to be called in test/mock code. The classes are instantiated with the type LsoSession<URLSession>
in production and something like LsoSession<MockNetworkSession>
in unit tests.
So there are two implementations of download(for:) in the class, I was expecting the more specific overload to be called in production and the general overload to be called in other cases (e.g. unit tests).
What happened in reality was the more general version was getting called in all cases.
So the if clause at the start of the more general version of the download method (the version without a generic constraint) should never be run. But it was. That's why I had to add that and use that rather inelegant if clause...
public class LsoSession<BaseNetworkSession: DownloadNetworkSession>: NSObject, DownloadNetworkSession, LongLifespanDownloadsNetworkSession {
public func download(for urlRequest: URLRequest) -> BaseNetworkSession.DownloadTask
where BaseNetworkSession == URLSession {
baseSession.downloadTask(with: urlRequest)
}
// this method should only be used in unit tests at the moment
public func download(for urlRequest: URLRequest) -> BaseNetworkSession.DownloadTask {
if let baseSession = baseSession as? URLSession {
guard let downloadTask = baseSession.downloadTask(with: urlRequest) as? BaseNetworkSession.DownloadTask else {
fatalError("impossible?")
}
return downloadTask
}
return DownloadTask()
}
private var baseSession: BaseNetworkSession
public init(baseSession: BaseNetworkSession) {
self.baseSession = baseSession
super.init()
}
I then looked at the call site...
extension FileDownloadService where BaseNetworkSession: LongLifespanDownloadsNetworkSession {
public func downloadFileInBackground() async throws -> BaseNetworkSession.DownloadTask {
let downloadTask = network.download(for: try request())
downloadTask.resume()
return downloadTask
}
}
which is an extension on...
public final class FileDownloadService<BaseNetworkSession: DownloadNetworkSession>: Service, GetMethod, LsoService
where BaseNetworkSession.DownloadTask: NSObject, BaseNetworkSession: NSObject {
public typealias RequestId = String
typealias Dependencies = RequestId
let network: BaseNetworkSession
...so I think in some way the compiler must be calling download via a witness table or something. I had thought the compiler would be able to work out the correct version of the method to call but it seems it's not.
Interested in people's thoughts on this. I think this is the sort of thing that will improve in Swift 6 because it will always have to be explicit when you're creating any kind of indirection through a witness table like this as I understand it because when polymorphism/indirection for protocol types goes via a witness table it will have to be annotated with any, which should hopefully make all this more clear. Possibly I'm misunderstanding it.