Nicer reuse identifiers with protocols in Swift
Recently I was going through the motions of setting up a new UICollectionView
. I had written a view model for my cells, and I had a UICollectionViewCell
subclass all ready to go. All that was left to do was to implement cellForItem(at:)
.
As an aside: if you’ve not worked with UICollectionView
before, but are more of a UITableView
kinda person, then you can just replace Collection
with Table
and Item
with Row
and this post should still be valid.
As a responsible iOS engineer [citation needed] I knew that the first thing I needed to do here was ask my collectionView
to dequeue a cell. And to do that I had to call dequeueReusableCell(withReuseIdentifier:for:)
, passing a String
“reuse identifier.” In order for the collectionView
to have any idea what I was talking about, I also had to call register(_:forCellWithReuseIdentifier:)
on the collection, so it knew to map this reuse identifier to my UICollectionViewCell
subclass.
That subclass looked roughly like this:
final class CustomCollectionViewCell: UICollectionViewCell { // code etc.}
Another aside: CustomCollectionViewCell
is just an example name. Please never name classes like this.
I decided a sensible reuse identifier for this cell would be "CustomCollectionViewCell"
. So I used that, and called both the methods. This is what those calls looked like:
// in a set up method
collectionView.register(
CustomCollectionViewCell.self,
forCellWithReuseIdentifier: "CustomCollectionViewCell"
)// in cellForItem(at:)
collectionView.dequeueReusableCell(
withReuseIdentifier: "CustomCollectionViewCell"
for: indexPath
)
This is obviously awful. We’ve got a magic string hanging around there, and it’s in more than one place. Clearly this is in need of some refactoring, so that’s exactly what I did. The first step was simply to move this string into a static constant on the CustomCollectionViewCell
class.
final class CustomCollectionViewCell: UICollectionViewCell { static let reuseIdentifier = "CustomCollectionViewCell"}
Okay, good start. This means we can access the identifier anywhere in the code using CustomCollectionViewCell.reuseIdentifier
. Already a massive improvement over what we had before. But it does present another problem. What if the class name changes later on?
We could just remember to change it, but let’s be real here: we’re humans, and we don’t always remember things like this. Even when it’s staring us right in the face. That’s why we get computers to do this stuff for us. So that leads nicely on to the next question: can we get the compiler to help us?
Well, if the answer was no, this would be an awfully short blog.
So how do we do it? See above, where we passed the reference to the CustomCollectionViewCell
type into register(_:forCellWithReuseIdentifier:)
? Wouldn’t it be great if we could convert that type into a String
somehow and just use that? That would be so great.
(Dramatic pause.)
Yep, of course we can do exactly that. We can replace the bare String
in the definition of reuseIdentifier
with String(describing: CustomCollectionViewCell.self)
and get exactly the same output. Now if the class name changes, the compiler won’t recognise the type there, and will let us know about it. Success!
Okay, right about now you might be looking at what we’ve done so far and thinking: “Matthew, this is great and all, but the title says ‘protocols’ and so far we’ve just pissed about with a bit of refactoring.” And you’re right, for sure. Consider all of the above simply as motivation for what comes next.
So after a while of using String(describing: TypeName.self)
to generate reuse identifiers, I started to wonder if there was a way of doing this that would remove the boilerplate of defining a new reuseIdentifier
in every cell subclass.
I started by writing the following protocol:
protocol ReuseIdentifying {
static var reuseIdentifier: String { get }
}
And then extending it like so:
extension ReuseIdentifying {
static var reuseIdentifier: String {
return String(describing: /* er... what goes here? */)
}
}
I got a bit stuck here for a while. I needed a way to dynamically refer to the type that was implementing the protocol. Obviously, using ReuseIdentifying.self
wouldn’t work, because that’s just going to return "ReuseIdentifying"
every time.
In the past I’ve used Self
in protocols to indicate “the implementing type.” So I wondered whether I could get away with something like Self.self
. It looks utterly ridiculous, but…
Spoiler: it totally worked.
So here’s the final protocol extension:
extension ReuseIdentifying {
static var reuseIdentifier: String {
return String(describing: Self.self)
}
}
Now by applying that protocol to my CustomCollectionViewCell
I can delete the reuseIdentifier
definition, and everything works as expected. The returned reuseIdentifier
is, as I wanted, "CustomCollectionViewCell"
.
And there’s even better news: we can go further with this. Let’s say we want every single UICollectionViewCell
subclass to have a reuseIdentifier
. We don’t actually need to individually apply this protocol. We can just do the following:
extension UICollectionViewCell: ReuseIdentifying {}
Magic! ✨
Open questions
I like to test things, so I wanted to write some unit tests against this protocol extension.
I tried defining a class in the body of a test. Something like:
func test_reuseIdentifier_classIsCalledSomeClass_isSomeClass() {
class SomeClass: ReuseIdentifying {}
XCTAssertEqual("SomeClass", SomeClass.reuseIdentifier)
}
As far as I could see, there’s no reason this wouldn’t work. But String(describing:)
starts to behave kind of confusingly at this point. The return value from reuseIdentifer
is "SomeClass #1"
.
I thought this might be an issue with having the class defined inside the method, so I moved it up to the file scope and ran the test again. And it worked!
So, if anyone knows why defining the class inside a method leads to the "#1"
being appended to the end of the description, I’d be super interested to find out.