Dequeueing Reusable Views With Generics and Protocols

In today's episode, I'd like to show you an elegant example of the power and versatility of generics and protocols. I came across this implementation while browsing the RxDataSources repository a few months ago. I learned the technique I outline in this episode from Segii Shulga. Let me show you what it looks like.

If you've been reading Cocoacasts for a while, then you know that I'm a fan of the elegance fatal errors can bring to a codebase. This is the approach I usually take to dequeue a table view cell.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "WeatherDayTableViewCell", for: indexPath) as? WeatherDayTableViewCell else {
        fatalError("Unable to Dequeue Weather Day Table View Cell")
    }

    if let viewModel = viewModel?.viewModel(for: indexPath.row) {
        cell.configure(withViewModel: viewModel)
    }

    return cell
}

The dequeueReusableCell(withIdentifier:for:) method returns a UITableViewCell instance. We cast the UITableViewCell instance to a WeatherDayTableViewCell instance. This operation should never fail. We throw a fatal error if it does. This works fine, but there's a better, more elegant solution that's easier on the eyes and avoids code duplication.

Generics and Protocols

The solution I lay out in this episode takes advantage of generics, protocols, and type inference. That's a lot of ingredients, but I promise you that it isn't as complex as it sounds.

Creating Extensions

Before we start playing with generics, I want to make sure a UITableViewCell subclass can return its reuse identifier. Why is that?

It's an essential ingredient for the solution we're about to implement, but it also provides convenience. We don't need to define constants or rely on string literals.

Create a new Swift file and name it ReusableView.swift. We define a protocol, ReusableView, with a static variable property, reuseIdentifier.

protocol ReusableView {

    static var reuseIdentifier: String { get }

}

This means that any type conforming to the ReusableView protocol can return its reuse identifier. But we take it one step further by providing a default implementation using an extension.

extension ReusableView {

    static var reuseIdentifier: String {
        return String(describing: self)
    }

}

Any type conforming to the ReusableView protocol has a static property named reuseIdentifier. The only thing left to do is conform the UITableViewCell class to the ReusableView protocol. Because we provided a default implementation for the reuseIdentifier property, we can leave the implementation of the extension empty.

extension UITableViewCell: ReusableView {}

Don't forget to add an import statement for the UIKit framework at the top.

Generics and Type Inference

It's time to add generics to the mix and take advantage of Swift's powerful type inference. Create a new Swift file and name it UITableView.swift. Replace the import statement for Foundation with an import statement for UIKit.

import UIKit

We define another extension. This time we create an extension for the UITableView class. We define one method, dequeueReusableCell(for:), and it accepts one argument of type IndexPath. Notice that it no longer accepts a reuse identifier as an argument.

import UIKit

extension UITableView {

    func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {

    }

}

This is the hardest part of the episode. Bear with me. The goal is to ask a table view for a reusable table view cell by providing nothing more than an index path. This is possible if we tell the compiler what type of table view cell we expect. Notice that the return type of the dequeueReusableCell(for:) method is of type T, a generic type.

We also define a type constraint in angle brackets. We define that the type of the return value needs to inherit from UITableViewCell. That opens up possibilities.

In the method's body, we can reuse the code snippet I showed you at the beginning of this episode. We dequeue a table view cell by invoking dequeueReusableCell(withIdentifier:for:). Because T needs to inherit from UITableViewCell and UITableViewCell conforms to the ReusableView protocol, we can ask it for its reuse identifier by accessing the value of its reuseIdentifier property. We cast the result to T just like we did at the beginning of this episode. If the cast fails, we throw a fatal error.

import UIKit

extension UITableView {

    func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
            fatalError("Unable to Dequeue Reusable Table View Cell")
        }

        return cell
    }

}

Generics In Action

It's time to put the solution we implemented into practice. We remove the guard statement and put the newly created method on UITableView to use. Notice that we no longer cast the dequeued table view cell to a WeatherDayTableViewCell instance. I'm sure you agree that this looks quite nice.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(for: indexPath)

    if let viewModel = viewModel?.viewModel(for: indexPath.row) {
        cell.configure(withViewModel: viewModel)
    }

    return cell
}

Something's not right, though. The compiler throws an error, telling us that the UITableViewCell class doesn't define a configure(_:) method. That's true, but why is it telling us this? We want to dequeue and return an instance of the WeatherDayTableViewCell class.

The compiler throws an error.

Remember that we leverage generics and type inference. We want to dequeue a WeatherDayTableViewCell instance, not a UITableViewCell instance. But how should the compiler know that we want to dequeue a WeatherDayTableViewCell instance?

The compiler tries to infer the type of the cell constant we define in tableView(_:cellForRowAt:). It sees that we return cell from tableView(_:cellForRowAt:) and, by inspecting the method definition, it infers that cell should be of type UITableViewCell. The compiler is correct.

The solution is very simple. When we declare the cell constant, we need to explicitly specify its type. The compiler then understands that the dequeueReusableCell(for:) method should return a WeatherDayTableViewCell instance.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell: WeatherDayTableViewCell = tableView.dequeueReusableCell(for: indexPath)

    if let viewModel = viewModel?.viewModel(for: indexPath.row) {
        cell.configure(withViewModel: viewModel)
    }

    return cell
}

By making it clear that we expect a WeatherDayTableViewCell instance, the dequeueReusableCell(for:) method can substitute its generic type for a concrete type. That's essential for this solution to work.

This pattern isn't that uncommon. Unbox is a library I frequently use for working with JSON. For Unbox to do its magic, you need to tell it what type it needs to deserialize for you.

let profileData: [ProfileData] = try? unbox(data: data)

Generics and protocols can be a powerful combination if used correctly and Swift's type inference is the glue that binds everything together.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By