Generic Protocols in Swift

Joe Williams
7 min readMar 6, 2021

--

I’ve recently been undertaking a rebuild of my app, resulting in me thinking about what the architecture looks like as a whole. I’d previously made use of IGListKit, and over a couple of years it had served me well. Yet, with the advancements in modern collection views, I was able to rethink the use of this dependency. As a result, it led me down a bit of a rabbit hole into generics across protocols.

I wanted to solve the problem of declaring the same three things for a view controller. My app is made up of, pretty much, five controllers. Four these controllers are pretty much like-for-like clones. I declare a view model, a service, a data source, a loading view, and finally an error view:

class DiscoverController: UICollectionViewController {     lazy var dataSource: ....
lazy var errorView: ....
lazy var loadingView: ....
private let service: Service
private let viewModel: ViewModel
}class SearchController: UICollectionViewController { lazy var dataSource: ....
lazy var errorView: ....
lazy var loadingView: ....
private let service: Service
private let viewModel: ViewModel
}

I wanted to consolidate these into one, understandable, generic “List” controller. I’d done this to some extent before, but wanted to think a little bit more about responsibilities too. We’ll get into that a bit later on. For now, an overview.

Generics

The standard library is littered with generics, and you’ve likely use them more than you know. The guiding principle of generics is that they allow you to add flexibility and reusability to your code. Let’s review the following example:

class APIService {    func request(url: URL) -> AnyPublisher<Foo, Error> {
return URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Foo.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}

This might be okay in a number of scenarios. However, as the data you consume becomes more complex, you’re going to hit snags. Generics provide a concise way of fixing this. There’s two possible solutions: a generic class or a generic method. The former limits all future calls on that property to a specific type, or creating a bunch of these for this call. That doesn’t feel right. Instead, we can use a generic function:

class APIService {func request<Decoded: Decodable>(url: URL) -> AnyPublisher<Decoded, Error> {
return URLSession.shared
.dataTaskPublisher(for: URL(string: "")!)
.map(\.data)
.decode(type: Decoded.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}let foo: AnyPublisher<Foo, Error> = service.request(url: fooUrl)
let bar: AnyPublisher<Bar, Error> = service.request(url: barUrl)

The caveat here is we have to be explicit with our type in the property declaration. It makes the code slightly more verbose, but the benefits are a worthwhile trade-off. We can now decode over anything that conforms to decodable. Great!

Generic Protocols

Extending this to protocols is fairly straightforward. There are two mechanisms for a generic protocol, one of which we’ve already covered. The first is generic functions on a protocol definition, whereby the caller determines the type at call-site like before. This is fairly useful, but doesn’t offer us the flexibility I needed in my re-write. I wanted to introduce Presenter’s into my app. Currently, all networking code was doing through the ViewController which I wasn’t happy with. It didn’t need to be a responsibility of the controller, so I aimed to pull it out. I naively began with the following:

protocol Presentable {
associatedtype ViewModel
var item: ViewModel { get }
func load()
}

Pretty good! We’ve got a generic protocol whereby in conformance I can declare the type of ViewModel:

class DiscoverPresenter: Presenter {
var item: DiscoverViewModel = .init()
func load() {
/// make network request
}
}

This was shaping up to really solve a problem for me. Until I came to actually use it. If you’ve ever used an associated type on a protocol before, I’m sure you’ll be horribly familiar with this scenario:

class CollectionController: UICollectionViewController {    /// Protocol 'Presentable' can only be used as a generic constraint because it has Self or associated type requirements.
private
let presenter: Presentable
}

The error message sounds complicated, but it is actually telling us what the issue is: you can only use Presentable as a generic. Why? Well, it can’t infer the associatedtype requirement because it hasn’t been satisfied. The rabbit hole started getting deeper, and I began looking into a variety of solutions

1: Opaque Types

Introduced in Swift 5.1, opaque types give us one potential solution. You can think of opaque types are reverse generics, and that’s the definition that really aided my understanding. You’ll be familiar with opaque types from SwiftUI:

struct ContentView: View {    var body: some View { ..... }}

The keyword some allows us to return a protocol that might have Self or associated type requirements. By leveraging the power of Swift’s type inference system it’s able to determine what the underlying type is without revealing the interface. That’s incredibly powerful. It means in my example I thought I could do something like this:

class CollectionController: UICollectionViewController {    private let presenter: some Presenter}

Sadly, I was now at another error:

Property declares an opaque return type, but has no initializer expression from which to infer an underlying type

Opaque types demand they we give them some indication of what the underlying type is, otherwise they can’t be utilised. I’m able to solve this by doing something like:

class CollectionController: UICollectionViewController {    private let presenterType: PresenterType    private var presenter: some Presentable {
return PresenterFactory.for(type: presenterType)
}
}

Although it solves our compile time error, it doesn’t feel right. I wasn’t happy with it for a couple of reasons. Firstly, I’d have to create some new enum that I inject in for the computed property to use to build a Presentable object. Secondly, I’d have to inject in a factory to aid in testability. Quite frankly, it’s too many extra steps for the benefit. It’s more to maintain and more to forget. As such, this didn’t feel like the correct use case for me.

That doesn’t mean there’s no use case for it. The error kind of explains this to us: it’s an opaque return type. Think of using opaque return types when you want the function to decide what gets return, and not the caller. Look back at the example with the API service earlier where we had to declare our return type at the call site. This is precisely where they’re powerful: when you have an associated type requirement and want the function to determine some protocol is returned.

2: Type Erasure

There are a bunch of different ways we can do type erasure in Swift. Whenever I’m writing API’s, I like them to mirror the standard library as much as possible. This way future developers working on a framework or project are greeted with a level of familiarity. As such, my type erasure approach aligns itself as closely as possible to the AnyHashable struct:

struct AnyPresentable<Model>: Presentable {    private let baseLoadImplementation: (() -> Void)    var item: Model    func load() {
baseLoadImplementation()
}
init<Presenter: Presentable>(_ base: Presenter) {
self.item = base.item
self.baseLoadImplementation = base.load
}
}let presenter = AnyPresentable<String>(MainPresenter())

There’s a couple of glaringly obvious bits here that make me feel squeamish. Firstly, we’ve got to make the wrapper generic over some Model type. When we come to use it in our generic controller we’re left with this:

class CollectionController: UICollectionViewController {    private let presenter: AnyPresenter<???>}

Of course, we could solve this issue by giving the controller a generic which we forward onto AnyPresenter :

class CollectionController<Model>: UICollectionViewController {     private let presenter: AnyPresenter<Model> }

It solves the problem, but it raises a question: if I’m passing through the Model as a generic, why don’t we just bypass the additional step of writing a type erasing wrapper? We spoke earlier about opaque return types as inverse generics. We’ve now come full circle and back to your average generic to solve the original issue.

3: Standard Generic on Concrete types

And so we come to the ultimate solution to the problem of associated type requirements on a protocol. This was also the solution I settled on and I’ll go into why shortly. First, let’s show how it works (though I’m sure you can guess!):

class CollectionController<Presenter: Presentable>: UICollectionViewController {     private let presenter: Presenter }let viewController = CollectionController<MainPresenter>(MainPresenter())

To me, this solution fits best for a number of reasons. It’s really readable, easy to reason about, and still leverages the flexibility associated types give us on protocols. It’s a familiar method that I’d also hope most developers would recognise.

Secondly, it also means I can take advantage of the associated type for the generics over the UICollectionViewDiffableDataSource on the concrete class. Of course, you could do this with the type erased solution above, but the cons outweigh the pros for that solution. Therefore, my final implementation felt quite neat:

class CollectionController<Presenter: Presentable>: UICollectionViewController {     private let presenter: Presenter    private lazy var dataSource = UICollectionViewDiffableDataSource<Section, Presenter.ViewModel>(...)}

This means that we can forward on the associated type to the UICollectionViewDiffableDataSource without having to pass in another generic. It felt like I was getting something for free as a result of the generics.

Wrapping up

This has left me with a generic controller that I can pass a presenter into and build up without repetition. It’s worth noting that as a controller’s requirements become more specialised, we shouldn’t fear pulling this out into a new controller. I’ve learnt from previous projects that trying to shoehorn specific functionality into generic classes results in confusion and poor maintainability. For here though, it’s a great solution.

There’s one final limitation I think are worth mentioning. First, I’ve not solved another familiar problem which is heterogenous collections represented in a list. For instance, if my list was [Foo, Bar, Baz] I have no way of representing this. Right now, the associated type in an array assumes all items in that array are of the same type, i.e. [Foo]. At the moment, this is my use case, but in the long run it might not be.

Nonetheless, we’ve covered three methods of generic protocols in Swift, each offering a unique solution to the problem: opaques, type erasure, and generic classes. I’m well aware this is a well covered topic, but I wanted to offer something to the conversation with a concrete example. Hopefully, you’ve taken some learnings away from this post that you can apply to your own apps!

You can view my app, Expodition, here, with a little bit of knowledge of what’s happening under the hood!

Happy coding!

--

--

Joe Williams

iOS Engineer @ Sky Betting & Gaming | Indie Dev @ Expodition Podcast App