Skip to content

Generic collection view datasources. Helping kill massive view controllers.

Update: since I published this post, I have refined the approach, and published it s a framework. Read more about it in this post:

Generic datasources and cells. Introducing FountainKit.

Much has been written about one of the most common anti-patterns when it comes to iOS development: massive view controllers. And with good reason.

Massive view controllers

A massive view controller, in a nutshell, is a subclass of UIViewController that does too much. A typical example would be a subclass of UICollectionViewController that implements the UICollectionViewDataSource and UICollectionViewDelegate protocols, and therefore keeps track of the data model that needs to be displayed, which usually means that it is also responsible of fetching and populating that data model… You get the point: a class that does not exactly conform to the Single Responsibility Principle.

We all have written huge view controllers that do too much, and that slowly drift towards tangled messes that we do not dare to touch anymore. In the particular case of view controllers that display collections of data (table views or collection views), it all starts with making those view controllers conform to the datasource and delegate protocols.

But with the help of Swift’s generics and associated types, we can extract much of the implementation of the datasource protocols, and make it highly reusable. And, as a side effect, we can make them highly testable.

Let’s focus on UICollectionView

Much of the discussion in this post can be extended to cover table view data sources, but I am going to focus on collection views, for two main reasons: I haven’t used a table view in ages, and I haven’t used a table view in ages.

The data collection

The one assumption I am going to make, even though it can be considered as leaking the abstraction, is that the data collection (the collection containing my model objects) is somehow going to be structured in a way that objects can be added fetched from it by passing an NSIndexPath. I am fine with this assumption because that’s what UICollectionViewDataSource assumes as well.

So, let’s say that I abstract my data collection behind a protocol similar to this:

The first question popping up would be: why a protocol? Because I want to support different data structures. I don’t know how many different data structures, and I don’t know how those might be actually structured, so, to me, that screams “POLYMORPHISM!!!”

Now, let’s say I just want to manage a flat array, to populate a collection view with just one section, without titles. I could write an implementation of the DataManager protocol like this:

Please notice how the protocol and its implementation are generic: I can make implementations of the DataManager protocol handle any type, relying on the compiler to enforce that I don’t make too many mistakes.

The data source

At this point, I have an abstraction that I can use to manage data of any given type, with any given structure. Now, all I need is my data source to consume that data manager.

Usually, all the meat of implementing the UICollectionViewDataSource protocol is in the collectionView:cellForItemAtIndexPath: method (pardon my old-skool syntax). That method needs to dequeue a cell, fetch the proper model object, and populate the cell with it. All three of those tasks are highly specific to either the collection view, the model object, or both.

So how can we make that method reusable? Let’s review the three responsibilities again: dequeuing (specific to the collection view), fetching model object(since we have already abstracted the data collection, it only depends on the index path) and populating the cell (which, in my opinion, should be a responsibility of the cell itself, and, most importantly, it depends on the cell being dequeued).

So, two responsibilities related to each other: that suggest that maybe we should extract them to a separate abstraction.

Consider the following protocol:

Basically, I am providing a collection view, an index path, and a model object, and expect, in return, a collection view cell ready to be displayed. (This might be a good moment to suggest reading some other posts about declarative vs imperative programming, wink wink, nudge nudge)

Now, my implementation of the UICollectionViewDataSource protocol can look like this (more about the generic constraints after the code):

Take another look at the class declaration, and its initializer:

Firstly, I am injecting the dependencies, both the implementation of the DataManager protocol (therefore, my data collection) and the CollectionViewCellPopulator protocol (the thing that will provide me a cell). This makes my class completely testable. I can mock whatever I want, whenever I want.

Secondly, I am enforcing that both generic types T and U implement the DataManager and CollectionViewCellPopulator protocols, with one constraint: the associated type in both protocol must be the same. In other words, whatever implementation of the DataManager and the CollectionViewCellPopulator protocols that I pass to my data source need to be able to deal with the same type of model object.

A cell and a cell populator

Let’s say, for the shake of the example, that I have a specific collection view cell called TabCell:

This cell’s code is very specific. And it is the way it should be. This cell renders the information provided in a specific object (Section) in a very specific way (fetching a description and adding it to a label). That makes all sense in the world. We are in the UI layer, we need to know what we want to display and how we want to display it.

Now, in order to populate this cell, I can write an implementation of the CollectionViewCellPopulator protocol like this:

This code is still specific, but only where it needs to be: I know this class is going to be used only to dequeue instances of TabCell, because I will take care of only using this class when I actually need to display a TabCell. That’s all this class does: dequeue the cell, and pass it a model object, so the cell itself can render itself. Notice also how the type of the data parameter is specific to this particular class.

We could discuss if this class should or should not set values of the cell’s outlets directly. To be honest, I am see arguments for and against both designs (either the cell popular dequeuing and setting outlets, or the cell popular only dequeuing while the cell sets its own outlets), but I have gone with one responsibility per class. I believe, in the long term, the extra granularity will pay off.

Anyway, the point is, this class is a concrete implementation of an abstraction, and as such, is allowed to be specific.

Putting it all together

It is time to put all the pieces together. All we need to do now, is create an instance of a collection view, an instance of a data manager specific to the data model we need for that collection view, an instance of a cell populator specific to that collection view as well, pass them to a datasource, and boom!

Any view controller wanting to insert this colelctionview into the UI would just need to do something like this:

Final thoughts

I don’t really like all those angled brackets in the declaration of the dataSource property, but I guess it is a small price to pay.

I also assume this might not be the best possible solution, or at least, that it is a first step towards an actually solution, but, so far, it works for me. It allows me to:

  • Reuse the DataSource class all over the place.
  • Reuse the FlatArrayDataManager class in many different places
  • Support sectioned collection views with just one extra class implementing the DataManager protocol
  • Render cells specific to the collections that I need to display, with a set of highly cohesive classes.
  • Unit test all the things!: cell rendering, datasource population, data structures…
  • Keep view controllers smaller.

So, in my book, that’s a win. Please let me know what you think.

2 Comments

  1. greg3z greg3z

    Hello Cesar,

    Really great article! I am currently using your CellPopulator protocol and I must say, I love it! I love the way it clearly separate the populating part of the job and how it reduces my controllers.

    That’s really great to abstract all the populating of the view, it makes the flow model -> controller -> (populator) -> view really smooth. But how would you make the other flow view -> controller -> model that abstract and that smooth? By the flow, I’m thinking of user interactions with the cell, like a touch or maybe a swipe on the cell and a touch on a button as in the mail app, or maybe a more complexe interaction like a button or a textfield in a cell.

  2. sexysoftwareengineer sexysoftwareengineer

    Well, what I usually do is not that different.

    To continue discussing the same example, TabsController would be responsible for creating the DataManager, the DataSource, and also an implementation of the UICollectionViewDelegate, which would be a class where I would also inject the DataManager. So, just to clarify, both DataSource and Delegate would be instances of different classes, and both would get a reference to the DataManager injected into them.

    Now, user interaction in the view controller will be delegated to that particular implementation of the UICollectionViewDelegate protocol, which should be able to fetch an specific model object (if needed) and do with it whatever is necessary.

Leave a Reply

Your email address will not be published. Required fields are marked *