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)
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)
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 {
}
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.