Let’s get meta: Swift metatypes and cell reuse
16 Nov 2015For the past few weeks, I’ve been running a small project called Public Extension. Each weekday1, I come up with a handy Swift extension (to the Standard Library, UIKit, etc.) and tweet it out for everyone to follow along!
One particular extension has been on the back of my mind for a few days now. Public Extension #9 (initial draft below)
The goal was to make class registration for UITableViewCell
s safer by avoiding a String
ly-based API that requires code to be updated in multiple places, when reuse identifiers change. To tackle this, I took the following approach:
- Define a
Reusable
protocol that requires classes to have a staticreuseIdentifier
member - Wrap the traditional
registerClass(_:forCellReuseIdentifier:)
to accept aReusable.Type
metatype, which hides the previously-neededforCellReuseIdentifier
parameter. - In my initial extension, I omitted the wrapper for
dequeueReusableCellWithIdentifier(_:)
anddequeueReusableCellWithIdentifier(_:forIndexPath:)
, but we’ll discuss these soon!
Note: This approach only works in code. Setting reuse identifiers in Storyboards requires you to manually enter a String
.
Before diving in and refining the above approach, let’s take a step back and define what Reusable.Type
and CustomCell.self
mean in the snippet above. They’re making use of Swift’s Metatype Type.
Swift Metatype Type
In The Swift Programming Language book, the metatype type is defined as follows:
“A metatype type refers to the type of any type, including class types, structure types, enumeration types, and protocol types.”
This is exactly what we need. Since our Resuable
protocol defines reuseIdentifier
as a static member, we can access it from the metatype of any class that conforms to it! To access a type as a value, we can use a postfix self
expression (i.e. .self
). For example, in our CustomCell
class above, CustomCell.self
returns CustomCell.Type
not an instance of CustomCell
.
Refining the Approach
Now that we’ve gone over metatypes, let’s make our approach to safe cell reuse better! We’ll keep the same Reusable
protocol but redefine our UITableView
extension as follows:
While we could have used Swift’s error handling, cell misconfiguration is tricky to recover from. If misconfigured, it might be misleading to return a plain UITableViewCell
. So, it’s probably better to lean towards a fatalError
to catch these issues during development.
Note that the return types for the dequeueReusable
overloads are non-optional. This is really handy because we no longer have to deal with awkward casts in tableView(_:cellForRowAtIndexPath:)
. Here is how the call site might look:
Extending UICollectionView
We can apply this same pattern to UICollectionView
as well:
A Note on Cell Subclassing
One of my friends, Hans, brought up a potential gotcha with regard to subclassing cells that implement Reusable
.
@jasdev @PublicExtension this doesn't allow for inheritance for classes that implement `Reusable`, right?
— Hans E Hyttinen (@Lumilux) November 5, 2015
The static
keyword in Swift translates to class final
2, meaning that the property is attached to a class but cannot be overridden. However, when defining Reusable
, we have to use the static
keyword (even though we have the explicit class
requirement on the protocol) because the class keyword is only allowed within class
definitions. To allow for subclassing of cells that conform to Reusable
, we can define reuseIdentifier
as a computed3 class var
.
It’s a little awkward that we can conform to a protocol (constrained to class
) with a static requirement using a class var
. Would love it if Chris Lattner or Joe Groff could chime in on this!
Summary
So, there we have it! We’ve abstracted away a String
ly-based API for cell reuse in table and collection views. Now, if we ever need to change reuse identifiers, all we have to do is update the the cell definition. This benefits from code locality. I had also thought about an enum
-based approach, but this would move reuse identifiers farther away from cell definitions, as enum
s can’t be defined across multiple files. However, the enum
approach has one advantage, which is guaranteeing unique reuse identifiers.
Hope this pattern is useful! If you have any feedback or suggestions, I’d love to hear from you!