Neil Macy

Dequeuing Custom UITableViewCells and UICollectionViewCells

When you want to use a UITableViewCell or UICollectionViewCell subclass, you need to go through a few boilerplate steps to register and dequeue it.

Usually in an initialiser or in the containing UIViewController's viewDidLoad method, you register a UITableViewCell or UICollectionViewCell subclass that can be dequeued with a unique identifier String:

tableView.register(MyCustomCell.self, forCellReuseIdentifier: "cell")

Then, you implement a UITableViewDataSource or UICollectionViewDataSource which contains the cellForRowAtIndexPath or cellForItemAtIndexPath method. In this method, you ask the table or collection view to dequeue the reusable cell that can be used for the current item in the data source, based on its reuseIdentifier:

let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCustomCell

The Problems

Using the Reuse Identifier in multiple places

You have to specify the reuse identifier twice - once when registering and once when dequeuing. But you could accidentally mismatch cell types and reuse identifiers, or make a spelling error in one place which means the correct cell doesn't get dequeued, or forget to update the dequeue call after changing the reuse identifier in the register call. (This can be mitigated by declaring a constant for each reuse identifier, but if you have multiple cell types, it can become a bit of a list.)

Casting

The bigger issue is that the cell that you're given back when dequeuing is of the base class type, UITableViewCell or UICollectionViewCell, and you have to cast that to the custom type that you explicitly registered.

So you then get the choice of using an optional cast with as?, and adding some handling for dealing with optionals, or force casting to your custom type using as!, as shown in the example above, and letting the app crash if it gets the wrong type back.

This shouldn't cause any problems if you do everything perfectly all the time, but if you changed the cell type for that reuse identifier when dequeuing, and forgot to change the cast after registering, your app will either show a broken UI, or crash, just because the type of cell needs to be declared in two separate places.

The whole approach is messy. Reuse identifiers aren't something we typically need to care much about - it's usually more of an implementation detail of the UITableView/UICollectionView. And considering we explicitly told the UICollectionView or UITableView what cell type to expect for a given reuse identifier, we should be able to use that cell type implicitly, rather than casting after dequeuing.

Here's how you can do that.

The ReusableView protocol

If we make all of our custom UITableViewCell and UICollectionViewCell subclasses conform to the ReusableView protocol below, they will have a property called defaultReuseIdentifier which can be used as the reuse identifier without having to name it explicitly.

public protocol ReusableView: class {
    static var defaultReuseIdentifier: String { get }
}

public extension ReusableView where Self: UIView {
    static var defaultReuseIdentifier: String {
        return NSStringFromClass(self)
    }
}

That means you can avoid copy/paste errors, and don't have to think about what value to give to your reuse identifier. You get a reuse identifier for free, using the class name. But it works even better with some extensions.

UITableView/UICollectionView extensions

These extensions add a generic method to UITableView and UICollectionView called dequeue, which lets you dequeue a cell without having to explicitly register it, or having to cast it to your custom type.

public extension UITableView {
    func register<T: UITableViewCell>(_: T.Type) where T: ReusableView {
        register(T.self, forCellReuseIdentifier: T.defaultReuseIdentifier)
    }

    func dequeue<T: UITableViewCell>(for indexPath: IndexPath) -> T where T: ReusableView {
        register(T.self) // this removes the need to explicitly register a cell
        guard let cell = dequeueReusableCell(withIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
            preconditionFailure("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
        }

        return cell
    }
}

public extension UICollectionView {

    func register<T: UICollectionViewCell>(_: T.Type) where T: ReusableView {
        register(T.self, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
    }

    func dequeue<T: UICollectionViewCell>(for indexPath: IndexPath) -> T where T: ReusableView {
        register(T.self) // this removes the need to explicitly register a cell
        guard let cell = dequeueReusableCell(withReuseIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
            preconditionFailure("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
        }

        return cell
    }
}

Using the register method, it will register cells with the default reuseIdentifier from ReusableView. It will then handle casting, throwing a preconditionFailure if the cast fails. But it shouldn't, because registering the cell for reuse happens at the same time as dequeuing a cell of that type.

Registering and Dequeuing Now

Conform your UITableViewCell or UICollectionViewCell to ReusableView:

class MyCustomCell: UITableViewCell, ReusableView {
    // your cell implementation
}

And declare the expected cell type when you dequeue your cell:

let cell: MyCustomCell = tableView.dequeue(for: indexPath)

That's it. You never need to specify a reuse identifier, you don't need to register your cell for reuse, and you don't need to deal with casting.

The Code

I wrote this into a Swift package on GitHub, called ReusableCells. It uses just three small files to do the above, so if you don't use Swift Package Manager, it's really easy to copy the code in yourself.

Published on 1 February 2020