RT

Easy Reusable Cells with Swift Protocols and Generics

One of the first things that most iOS developers learn is how to present data in table or collection views, which involves registering and dequeuing reusable cells using string identifiers. The typical code that I see around usually looks like this:

class MoviesController: UITableViewController {

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MovieCell.self, forCellReuseIdentifier: "MovieCell")
    }

    // ...

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! MovieCell

        // ...

        return cell
    }

}

A safer version of this code replaces the identifier strings with a constant and the forced typecast with a guarded precondition.

class MoviesController: UITableViewController {

    static let cellIdentifier = "MovieCell"

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MovieCell.self, forCellReuseIdentifier: Self.cellIdentifier)
    }

    // ...

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellIdentifier, for: indexPath) as? MovieCell else {
            preconditionFailure("The returned cell is not a MovieCell")
        }

        // ...

        return cell
    }

}

This approach works well for table or collection views of a single cell type. But it gets a bit overwhelming if you need to support multiple cell types in a single collection.

Easy Reusable Cells ✨

A pattern that I have been using for quite a while is something that I call Easy Reusable Cells. This technique allows to register one or more cells without explicitly providing cell identifiers as follows:

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.registerCells([
        AdCell.self,
        MovieCell.self,
        FeaturedMovieCell.self,
    ])
}

But it also allows for reusable cell dequeuing in a strongly typed way:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...

    let cell = tableView.dequeueCell(MovieCell.self, for: indexPath)
    cell.configure(movie)
    return cell

    // ...
}

The heavy lifting is done behind the scenes by a protocol and a couple of extensions, all contained in a single file that I always add to my projects:

//
//  ReusableCell.swift
//

import UIKit

/// A protocol that all reusable cells must implement.
protocol ReusableCell: AnyObject {
    static var cellIdentifier: String { get }
}

// Default protocol implementation
extension ReusableCell {

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

}

// MARK: - Table View

extension UITableViewCell: ReusableCell {}

extension UITableView {

    /// Registers a set of classes for use in creating new table view cells.
    /// - Parameter cellTypes: The cell types to register
    func registerCells(_ cellTypes: [ReusableCell.Type]) {
        for cellType in cellTypes {
            register(cellType, forCellReuseIdentifier: cellType.cellIdentifier)
        }
    }

    /// Dequeues a reusable table cell object located by its type.
    /// - Parameters:
    ///   - type: The type of the cell to dequeue.
    ///   - indexPath: The index path specifying the location of the cell.
    /// - Returns: A valid cell instance of the specified type.
    func dequeueCell<T: ReusableCell>(_ type: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(
            withIdentifier: type.cellIdentifier,
            for: indexPath
        ) as? T else {
            preconditionFailure("Unexpected cell type returned")
        }

        return cell
    }

}

// MARK: - Collection View

extension UICollectionViewCell: ReusableCell {}

extension UICollectionView {

    /// Registers a set of classes for use in creating new collection view cells.
    /// - Parameter cellTypes: The cell types to register
    func registerCells(_ cellTypes: [ReusableCell.Type]) {
        for cellType in cellTypes {
            register(cellType, forCellWithReuseIdentifier: cellType.cellIdentifier)
        }
    }

    /// Dequeues a reusable cell object located by its type.
    /// - Parameters:
    ///   - type: The type of the cell to dequeue.
    ///   - indexPath: The index path specifying the location of the cell.
    /// - Returns: A valid cell instance of the specified type.
    func dequeueCell<T: ReusableCell>(_ type: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(
            withReuseIdentifier: type.cellIdentifier,
            for: indexPath
        ) as? T else {
            preconditionFailure("Unexpected cell type returned")
        }

        return cell
    }

}

You can easily adapt this code to work with Nibs, if needed, by adding a few lines of code. 🪄