Why should I opt into programmatic UITableViewCell’s layout?

Yury Buslovsky
8 min readMay 11, 2020

--

Screenshot of the example app

TL;DR: Interface Builder cannot connect IBOutlets and IBActions to a generic class, whereas generics are a neat way to use UITableViewCells. This article demonstrates a pretty structured, yet very complex approach based on reusable data provider classes. You can find a sample project here.

Introduction

At some point, almost every developer who works as a contractor runs into yet another retail app. Most of those contain 2 main features: a feed-like screen and a product’s card. Let’s assume the former utilizes some kind of server-driven UI and the latter has static content sections. Both consist of many different UI blocks and they are likely to have common elements, say, a catalog with recommendations, which means that there is a need to reuse the block. Now it’s clear we should use a dynamic UITableView in both cases (instead of static one for the product’s card) and consider the recommendations catalog a dequeueable UITableViewCell. But without refactoring, it’s bound to result in unmanageably enormous table view’s data source/delegate methods. So, here’s the thing — why not reuse those methods as well as cell’s UI? This is where I begin elaborating on table cell providers.

Different tables, same cells

Table Cell Providing Protocol

Basically, table cell provider is an entity which reflects and combines UITableViewDataSource and UITableViewDelegate protocols. Each cell (not the entire table view) has its own provider.

First and foremost, we have to provide a protocol, otherwise we won’t be able to store providers in an array within a view controller (a base class instead of the protocol is not an option. It is going to become more understandable down the road).

Let’s break it down:

  • A provider requires 3 elements: a data source, a delegate and the cell itself. We know nothing about their concrete types, so it’s time for generics. Swift’s powerful generics’ system allows for protocols with associated types, which basically are substitutes of generic types in structs, enums and classes. The data source has no constraints, it might be a struct as well as a class, if reference semantics is needed (for instance, when the data should be mutable). The delegate must be a class, because a provider is required to have a weak/unowned reference to the delegate, thereby preventing inevitable retain cycles. The cell, obviously, has to be a UITableViewCell subclass.
  • We store the aforementioned data source and delegate in an instance of a provider so as to have a way to reach them from within provider’s methods. The same applies to an enclosing table view and a corresponding index path. These objects are passed in to the designated initializer, except for the index path, because it just makes more sense to store it from the respective tableView(_:cellForRowAt:) method where it’s calculated automatically.
  • The rest of the protocol is somewhat convenience getter and setter for the data source and a bunch of methods mirroring those of the table view’s data source/delegate protocols. I have added only few of them for the example, but feel free to expand this protocol in your own project if needed.

Type-erased provider

As I mentioned before, we are going to store providers in an array. It should be something like var cellProviders: [TableCellProviding] = [...], right? Well, not exactly. Our protocol contains associated types which makes this impossible to apply it as a valid independent type. Right now we only can use it as a generic constraint, that’s why we’ll have to resort to the type erasure technique. There is a plethora of ways to implement it, but in our case a non-generic AnyTableCellProvider wrapper seems to be the best fit (we can’t opt into a generic AnyTableCellProvider<DataSource>, because there’s no way to generalize the data source’s type, so we’ll have to narrow it down to Any. And this is exactly why we couldn’t use a base class instead of the protocol, it’s just not the best practice (more like a code smell) to use Any directly, without wrapping it first).

This is what it looks like:

What it does is wraps a concrete provider, which conforms to TableCellProviding protocol, and downcasts its underlying data source type to Any (and the other way around). Since this class is concrete, its instances can be stored in an array, which was exactly our goal.

The only downside here is that we have to specify the type of data when calling getDataSource() method and rely on runtime crashes when calling set(dataSource:) method (unfortunately, it’s the only way to catch a misused data source’s type in setter. Swift’s type inference won’t be much of help here, thanks to the Any wrapper).

Base table cell provider

Seems like it’s time we created some real table cell providers! Let’s start with a base class in order to considerably minimize code duplication in providers. But first we should introduce some auxiliary entities and extensions:

  • ReusableView protocol which provides a reuse identifier for a cell.
  • Extension for UITableView which provides a very neat way to register and dequeue cells utilizing the ReusableView protocol and, once again, the brilliant Swift generics’ system.
  • ConfigurableTableCell protocol — a way to refer any cell from a provider. It helps to generalize cell’s setup logic. It also contains associated types for the data source and the delegate.

Feel free to check out the implementation here.

Now back to BaseTableCellProvider:

  • The base class is also going to be generic to facilitate reusability of stored properties preventing their redeclaration over and over again in new providers. We have to make sure associated types of all protocols in action (and the base class itself) are the same. Then, we constrain the Cell type to conform to ConfigurableTableCell, thus getting a chance to configure the cell regardless of its underlying type.
  • In the initializer we register the cell. It relieves a view controller of ever doing it again.
  • Next up, we give optional methods a default empty implementation. This is necessary because of the Swift’s bug with dynamic method dispatch in protocols. More info here (Errors and Bugs section, SR-103).
  • Besides making our base class adopt TableCellProviding, we give the default implementation to methods which are not considered to be overridden in subclasses.

Why cells have to stay generic

The thing is, provider communicates with controller through the delegate protocol (it helps to handle cases such as when, for instance, the cell provides a button whose action should perform segue to another view controller meaning we have to hand over control through a delegate). And our goal is still reusability, which is why different controllers are able to adopt this delegate. So, we can only replace Delegate generic with the cell’s delegate protocol. But there’s a “but”: we can’t do that :D. Delegate placeholder declares the AnyObject constraint, so, since no protocol is an object (even when it has class constraint), we have to use some type which conforms to the delegate protocol:

protocol SomeCellDelegate: class { ... }

We can’t do the following:

final class SomeCellProvider: 
BaseTableCellProvider
<
SomeDataSource,
SomeCellDelegate,
SomeCell
> { ... }

because SomeCellDelegate is not a concrete class type. We’ll have to do something like that:

final class SomeCellProvider<Delegate: SomeCellDelegate>: 
BaseTableCellProvider
<
SomeDataSource,
Delegate,
SomeCell
> { ... }

This way, we make sure Delegate is a class type, because SomeCellDelegate is only available for classes. The same applies to the cell classes hierarchy, if we try to structure it with similar intentions of eradicating stored properties’/setup methods’ boilerplate code in the leaves of OOP-tree.

Here goes BaseTableCell:

It shows the approach similar to BaseTableCellProvider’s one.

Example

Now, let’s take a look at concrete cells and providers. For example, let’s say your app has this cell:

It displays a title, and a details screen appears after the button is tapped. Let’s name it DisclosureCell.

That’s all you have to do to provide appropriate business logic:

I prefer using delegate extensions with Self constraints to keep cell’s business logic consistent and transparent. It also helps to remove extra code from massive view controllers. It’s up to you where to give the delegate’s methods an implementation.

The cell itself:

Programmatic UI is the only way to keep the cell’s class generic.

Let’s look at the view controller:

And that’s how table view’s data source’s and delegate’s implementations look now:

Neat, huh? No more M(assive)V(iew)C(ontroller)!

Conclusion

Generics are a great way to keep your code clean. Don’t let Objective-C’s kinks hinder it’s scalability! Don’t be afraid of generic UIView subclasses, you always have programmatic UI as an option.

Speaking about providers, I advise you to use this approach only when necessary. It might be a huge overhead for small projects with simple tables, etc.

Check out the full project here. There’s a separate branch xib-version where you can find a way to incorporate providers for cells with Interface Builders’s UI. It has more boilerplate code due to non-generic cells.

If you find programmatic UI inconvenient, I highly recommend trying SnapKit framework, so as to mitigate the “pain”.

Thank you for reading!

--

--

Yury Buslovsky

 iOS Software Engineer @ Revolut • 🇵🇹 Porto, Portugal