This blog post shows how to implement a set of UITableViewCell variants by using Protocol Oriented Programming (POP) as a better alternative to subclassing or composition.
NOTE: This material has been inspired by these two great write-ups on this topic:
Show me the cells!
While building my Cast Player app, I needed to add a couple of settings pages to let the user tweak a few things in the app, as well as include links to a Send Feedback form and some About screens. This is the end result:
From the screens above, we can identify six different types of cells:
|Cell name and image||Description|
|DisclosureTitleTableViewCell||A cell with a title and a disclosure chevron for navigation|
|BytesCountTitleTableViewCell||A cell with a title and a human readable byte count|
|DisclosureBytesCountTitleTableViewCell||A variant of the above with a disclosure chevron|
|ActionTitleTableViewCell||An action cell with a title but no disclosure chevron|
|ValuePickerTableViewCell||A cell for selecting amongst a set of values|
|ValuePickerLabelTableViewCell||A variant of the above with a title|
As a whole, these cells share the following traits (or behaviours):
- Cell Highlighting
- Display a title
- Display a human readable byte count
How can we go about building a set of classes that well represents these six variants of cells? A good first step is to list all the cell types and required traits in a grid:
|Cell name and image||Highlight Support||Display Title||Display Byte Count|
As we can see from the table above:
- None of the traits are adopted by all cells.
- Some of the traits are required for some cells but not for others.
This means that building a UITableViewCell class hierarchy would not work well here. In fact, none of these cell types is even a good candidate to be the base class. What to do? 🤔
Protocols & extensions! Yay!
Luckily, this great post about Mixins and Traits in Swift 2.0 can point us in the right direction. Intuitively, protocols and extensions could really work well for this use case, but how to use them correctly here?
The idea is that we can define the interface for each of our traits with a protocol, and provide a default implementation with an extension. Then, we can create very small subclasses of UITableViewCell and have them just conform to the protocols as needed. A very similar problem is explored in this other great talk named Introduction to Protocol-Oriented MVVM. Let’s see how this works!
Armed with protocols and extensions, we can write the first trait, TitlePresentable:
The second trait, BytesCountPresentable takes a similar form:
With these two traits alone, the implementation of the BytesCountTitleTableViewCell cell becomes trivial:
By simply conforming to the TitlePresentable and BytesCountPresentable protocols, the BytesCountTitleTableViewCell class inherits the behaviour added by the corresponding extensions. Other classes can inherit the same behaviour by means of protocol conformance. This is very powerful! 💪💪
Note that the class needs to redeclare the titleLabel and bytesCountLabel properties to correctly implement the protocols, but this is ok as we also need to specify that these are IBOutlet variables so that we can link them from Interface Builder.
What about the cell highlighting trait?
Let’s start from this class:
Here we choose to implement cell highlighting by overriding the setHighlighted() method of UITableViewCell.
Following the same approach outlined above, we could define a HighlightableView protocol and extension like so:
Then, we could try to create a UITableViewCell subclass that conforms to HighlightableView and see if cell highlighting works. No such luck unfortunately. 🚫
I believe this is because when the setHighlighted() method is called, the base UITableViewCell method will be called rather than the protocol extension method.
In other words, protocol extensions are meant to add behaviour to existing classes, but cannot be used to replace method overriding.
Cell highlighting, reloaded!
To make cell highlighting work, we can simply keep the HighlightableTableViewCell class defined above and subclass from it where necessary. Example:
This works nicely as we can simply choose to use HighlightableTableViewCell or UITableViewCell as a base class depending on whether we need the cell highlighting trait or not, and mix and match the remaining TitlePresentable and BytesCountPresentable traits as needed.
The resulting class hierarchy is summarised as follows:
In the code, this is represented as:
NOTE: HighlightableTableViewCell is the only trait that is implemented by subclassing and can serve as base class for three additional cell types. Once the base class is chosen for a given type, additional traits can only be added via protocol extensions.
Using Swift protocols and extensions as a way to add behaviour (traits) can lead to big wins in the design of our classes and helps keeping hierarchies flat. 🚀 Main advantages:
- Flatter class hierarchy
- Resulting APIs/classes can be more easily extended
- Easier to add and remove protocol conformance than adding and removing code
- Less code duplication
The solution presented in this post has worked well for me and my specific use case - I hope that this practical example helps understanding how to use protocol extensions better than I did when I started. If you know of a better way of doing this, let me know in the comments!