Dequeuing and registering cells for table and collection views has always been a little bit messy. UITableView was first released in iOS 2.0 and we have seen some significant improvements to the API since then.

iOS 6.0 gave us the register and dequeueReusableCell methods to help with performance issues people were having. At the time Swift didn't exist. if cell != cell and throwing around strings for registering these cells was, and still is, considered normal.

Now that Swift 5 is out and the language seems to be stabalizing, I hope that Apple  can begin to rewrite some UIKit components to be a little more Swift friendly.

Until then, we can do some of the work ourselves and hide these verbose Objective-C methods away in extensions, and remove the stringly typed reuse identifiers.

UITableViewCell

One of the first things we'll do to clean things up a bit is hide away that reuse identifier. You shouldn't have to deal with that (unless you want to).

extension UITableViewCell {
    
    static var reuseIdentifier: String {
        return NSStringFromClass(self)
    }
    
}

This will generate a reuse identifier for you based on the name of the class. So if your class was called TweetTableViewCell, then your reuse identifier would be the same. If it's in a separate module, it'll even be prefixed with that modules name.

Now you don't have to worry about remembering what this identifier is, or accidentally misspelling it.

Perfect, so let's use it to register our cell.

UITableView

extension UITableView {
    
    public func register<T: UITableViewCell>(cellClass: T.Type) {
        register(cellClass, forCellReuseIdentifier: cellClass.reuseIdentifier)
    }
    
}

All we need to pass in is the cells class. tableView.register(cellClass: MyCell.self). Simple.

Okay, but what about dequeuing?

The problem I have with the current dequeue methods is that what I get back is usually an optional. So your code looks like:

let cell = tableView.dequeueReusableCell(withIdentifier: id, forIndexPath: indexPath) as? MyCell

Then you have to unwrap it because cellForRow won't let you return an optional. Of course we could create our own wrapper around cellForRow — you can do this if you wish — but I want to clean up the code in cellForRow because that's what I'm going to be looking at every day.

extension UITableView {
    
    public func dequeue<T: UITableViewCell>(cellClass: T.Type) -> T? {
        return dequeueReusableCell(withIdentifier: cellClass.reuseIdentifier) as? T
    }

    public func dequeue<T: UITableViewCell>(cellClass: T.Type, forIndexPath indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(
            withIdentifier: cellClass.reuseIdentifier, for: indexPath) as? T else {
                fatalError(
                    "Error: cell with id: \(cellClass.reuseIdentifier) for indexPath: \(indexPath) is not \(T.self)")
        }
        return cell
    }
    
}

Generics to the rescue again!

let cell = tableView.dequeue(cellClass: MyCell.self, forIndexPath: indexPath)

Now we have an unwrapped cell that's guaranteed to be the type we passed in, all without having to worry about reuse identifiers.

Should anything go wrong, you'll get a fatal error at runtime. Which isn't ideal for everyones use case, but it works for me. An alternative would be to mark this function with the throws keyword and introduce error handling that way.

This idea can be applied to collection views and other register/dequeue functions that table views make use of (section headers, footers, etc).

It's not groundbreaking but I find that it has had a positive impact on the readability and conciseness of my view controller code.