An easier way to dequeue cells in iOS
Using the power of generics to improve cell registration and reuse π€
The problem we are trying to solve
Whenever we have to deal withUICollectionView
or UITableView
, we need to register a cell before we use it, for example.
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Foo")
Later, when you need to use it β¦
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Foo", for: indexPath) as! Cell
This is what Apple gives us out of the box but what could be wrong with that? π€·ββ . Well, for starters, we are dealing with a stringly-typed API. We have to register and dequeue our cells based on a string identifier. These two can easily get out of sync and can become a pain to maintain. Not to mention, we will only know its broken in production!
When we dequeue our cells, we also have to make a force cast to the cell type. It would be nice if we could avoid this entirely. Lastly, we have to remember to register the cell. What if we could have it all and automatically register the cell the moment we tried to dequeue them π€
The proposed solution
Our aim is to simplify cell reuse and registration to just one line of code
let cell: Cell = collectionView.dequeueReusableCell(for: indexPath)
This seems like a job for generics π
The solution here is inspired by this gist.
While generics are a fairly advanced topic, they are one of the most powerful features of Swift. For those new to Generics.
Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.
A large portion of the Swift standard library is built with generic code.
Since we would like to work with both UICollectionView and UITableView, we can start off by defining a protocol for a reusable view that has a defaultReuseIdentifier.
public protocol ReusableView: class {
static var defaultReuseIdentifier: String { get }
}
Since cell registration always needs a reuse identifier, letβs use the class name as the default identifier. We seldom need different reuse identifiers for the same view. Letβs use String(describing: self)
.
extension ReusableView where Self: UIView {
public static var defaultReuseIdentifier: String {
return String(describing: self)
}
}
One thing to be mindful of, is that we can also load our cells from a nib file. So lets go ahead and add a protocol for a nib loadable view.
public protocol NibLoadableView: class {
static var nibName: String { get }
}
extension NibLoadableView where Self: UIView {
static var nibName: String {
return String(describing: self)
}
}
Now that we have our ReusableView
& NibLoadableView
protocol, we can implement a generic way to register and dequeue a cell:
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) where T: ReusableView {
register(T.self, forCellReuseIdentifier: T.defaultReuseIdentifier)
} func register<T: UITableViewCell>(_: T.Type) where T: ReusableView, T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellReuseIdentifier: T.defaultReuseIdentifier)
}
This can be used on any UITableView
instance. Notice how we have two register functions. One is for ReusableView
and the other one is for NibloadableView
. When you register it will always look like table.register(CustomCell.self)
and depending on the protocol that the cell conforms to, it will use the relevant one. Try it out π
Now that we have a way to register, lets put together a nice way to dequeue our cells
func dequeueReusableCell<T: UITableViewCell>() -> T where T: ReusableView, T: NibLoadableView {
register(T.self)
return dequeueReusableCell(withIdentifier: T.defaultReuseIdentifier) as! T
}func dequeueReusableCell<T: UITableViewCell>() -> T where T: ReusableView {
register(T.self)
return dequeueReusableCell(withIdentifier: T.defaultReuseIdentifier) as! T
}
Every time we dequeue we call register first and in our experience the performance cost is negligible.
Mission accomplished π
Now that we have everything put together, this is how you would use it
//make your subclass conform to the protocol
extension CustomCell: ReusableView {}//then you would dequeue your cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CustomCell = collectionView.dequeueReusableCell(for: indexPath)
return cell
}
Thatβs it! We have removed so much boilerplate code now and our lives are so much easier. We no longer have to worry about registering cells and supplementary views. We never have to think about what string identifiers are in use and best of all, its all typesafe. With just two lines of code you are good to go π
Download the full code here