Typed, yet Flexible Table View Controller

January 5, 2016

This article uses Swift 2.1.

UITableView is a bread and butter of (almost) all iOS developers.
In most cases we simply present a single data type using the same UITableViewCell class and reuse identifier for all items.
Guys from objc.io got us covered here.
Situation gets more complicated when we want to have two, or more, different types of cells in one table view.
Heterogeneity of items makes things hard to implement in context of the type system.

This article shows three approaches to this problem. Each one tries to fix issues found in its predecessor. First approach reminds something that can be seen in many Objective-C codebases. Second one leverages enumerations which turn out to not be the best fit for this problem. Third, and final, implementation is built on protocols and generics. It's a good citizen in the world of Swift.

Basics

I'll work you through an example (it's on GitHub) of creating a table view with two types of cells: a) with a text, and b) with an image, as shown on the screenshot below:

UITableView displaying two types of data (text and image)

I'm fond of creating value types to encapsulate all the information a view needs to be able to configure itself. Let's call them view data. In our case they're really simple:

(In a real-world project™ they'll surely have more properties; image property would be of NSURL type to remove dependency on UIKit.) We'll also have two cells that can be updated with those view data structs:

With this in place we're ready to start working on our view controller.

1st Approach: “Easy”

I like to not overcomplicate things in the beginning by starting with an easy implementation that puts something on the screen.

We want our table view to be driven by the data in an array (property called items). Since we use structs that don't have anything in common, it has to be of type [Any]. We use the standard table view's reuse mechanism by registering cells beforehand in registerCells(). In tableView(_:cellForRowAtIndexPath:) we check the type of a view data at a given index path and dequeue and setup a cell accordingly. Full implementation of our view controller is quite concise1:

This implementation of course works, but I can think of at least a few reasons why it makes me 😞:

We can't make this view controller reusable. If we decide to add a new type of cell, e.g. for video, we'll have to change the code in three places: 1) introduce a new reuse identifier, 2) registerCells(), and 3) tableView(_:cellForRowAtIndexPath:).

If we decide to change items to be var and someone provides us with a view data type that we don't handle, we'll hit fatalError() in tableView(_:cellForRowAtIndexPath:).

We know that there's some relationship between a view data and a cell but it's not present anywhere in the type system.

2nd Approach: Enumeration

We can tackle some of these issues by introducing TableViewItem enumeration covering all acceptable types of view data:

The sad thing is, though, that we have to keep writing those switch statements all over the place. At the moment we've got only two of them, but it's not hard to imagine a need for more. If, for example, Auto Layout became a performance bottleneck and we moved to using manual layout, we'd have to use another switch in tableView(_:heightForRowAtIndexPath:).

I certainly could live with this implementation but I couldn't get those switch statements out of my head, which kept me going.

3rd (Final) Approach: Protocols and Generics

Let's forget everything we did in 1st and 2nd approaches and start again from scratch.

Updatable

We operate on cells that can be updated with some view data, so let's introduce Updatable protocol with an associated type called ViewData:

It's a generic struct with Cell type parameter. This type parameter is constrained: it has to conform to Updatable and be a subclass of UITableViewCell.

CellConfigurator has three properties: viewData, reuseIdentifier and cellClass. viewData's type depends on the type of Cell and it's an only property without a default value. Values of two other properties depend on the concrete type of Cell parameter (which as a newcomer to Swift I find really cool!).

...// further part of CellConfiguratorfuncupdateCell(cell:UITableViewCell){ifletcell=cellas?Cell{cell.updateWithViewData(viewData)}}}

Finally, we've got updateCell() method that takes UITableViewCell instance and updates it with viewData. We don't use Cell type here because UITableViewCell object is what dequeueReusableCellWithIdentifier(_:forIndexPath:) returns. Whew, that was a long description for a relatively short implementation.

That's because CellConfigurator is generic, however Swift's Array is homogeneous. So we can't simply put CellConfigurator<TextTableViewCell> and CellConfigurator<ImageTableViewCell> instances in one array. This could stop us for good, but it won't.

After some time there comes the aha! moment 💡. Cell type parameter is used only in the declaration of viewData. So, we can hide the exact Cell type used in CellConfigurator from a client of the API by adding a non-generic protocol:

There are still some steps left to do to make our view controller reusable, like allowing items to be changed from the outside. We won't go through them here, but you can see the final implementation divided into a framework and an example app on GitHub: ConfigurableTableViewController.

Conclusion

Let's see how our final implementation stands against the painpoints from first two approaches:

We don't have to touch the view controller at all if we want to add a new type of cell.

The view controller is type safe. We'll get a compile-time error if we provide a view data and a cell that don't cooperate with each other.

We don't have any switch statements that need to be constantly updated.

So, it looks like the 3rd approach solved all our issues 🎉. I think that we learned, once again, that it's worth pushing forward. A better solution is often just around the corner.

Thanks to Maciej Konieczny and Kamil Kołodziejczyk for reading drafts of this.

ViewController is a data source of the table view to keep an example concise. In a real project you may want to extract the data source into a separate object. ↩