Swift: Generic protocol (p.1)
In this article, we would look into how to implement a generic protocol. A few lacks of language support (Swift version prior to 3.0) would be also pointed out for discussion.
Let’s get started with an example. We are writing a bunch of code to implement data fetching used for a specific screen. We expect to use the same behavior for different types of data, different screens. Here’s the protocol:
protocol DataFetchingType {
associatedtype DataType
func fetchFromLocal(from from: NSDate, to: NSDate, completion: [DataType]? -> Void)
func fetchFromServer(from from: NSDate, to: NSDate, completion: [DataType]? -> Void)
}
The protocol should have an associated type DataType
which is particular for an api service.
OK! The next step is to create concrete classes that implement this protocol.
class EventsFetcher: DataFetchingType {
typealias DataType = Event
func fetchFromLocal(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
// Implementation goes here
}
func fetchFromServer(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
// Implementation goes here
}
}
class OrdersFetcher: DataFetchingType {
typealias DataType = Event
func fetchFromLocal(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
// Implementation goes here
}
func fetchFromServer(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
// Implementation goes here
}
}
So far, it’s good. Now we put a data fetcher in a view model which is also generic.
class ViewModel<DataType> {
var fetcher: DataFetchingType
...
}
Actually, XCode yields an error, complaining that DataFetchingType
is a generic protocol and thus cannot be used in such a way like that. More painfully, the current version of Swift does not yet support writing it similarly to generic class
var fetcher: DataFetchingType<DataType>
Solution
Here comes the trick to overcome this problem. The idea is simple, we use a generic class to wrap the protocol.
class GenericDataFetcher<DT>: DataFetchingType {
typealias DataType = DT
............
}
class ViewModel<DataType> {
var fetcher: GenericDataFetcher<DataType>
}
It’s still generic and the compiler would not complain. And the hard part remaining is to make GenericDataFetcher
flexible that we WOULD NOT write EventFetcher
as a subclass of GenericDataFetcher
(subclassing is not really a good idea here).
A solution: init GenericDataFetcher from a protocol, store the required functions, properties of that protocol.
class GenericDataFetchingType<DT>: DataFetchingType {
typealias DataType = DT
private var _fetchFromLocal: (from: NSDate, to: NSDate, completion: [DT]? -> Void) -> Void
private var _fetchFromServer: (from: NSDate, to: NSDate, completion: [DT]? -> Void) -> Void
init<DF: DataFetchingType where DF.DataType == DT>(fetcher: DF) {
self._fetchFromLocal = fetcher.fetchFromLocal(from:to:completion:)
self._fetchFromServer = fetcher.fetchFromServer(from:to:completion:)
}
func fetchFromLocal(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
_fetchFromLocal(from: from, to: to, completion: completion)
}
func fetchFromServer(from from: NSDate, to: NSDate, completion: [DataType]? -> Void) {
_fetchFromServer(from: from, to: to, completion: completion)
}
}
class ViewModel<DataType> {
var fetcher: GenericDataFetcher<DataType>
init<DF: DataFetchingType where DF.DataType == DataType>(fetcher: DF) {
self.fetcher = GenericDataFetcher(fetcher)
}
}
let eventsViewModel = ViewModel(fetcher: EventsFetcher())
P/s: I wish we would not need this trick any more in the near future.
Reference
- http://krakendev.io/blog/generic-protocols-and-their-shortcomings
- https://milen.me/writings/swift-generic-protocols
- https://github.com/apple/swift-evolution/tree/master/proposals (with keywords: generic, protocol :D)
Have fun!