DEV Community

loading...
Clue

Nicer reuse identifiers with protocols in Swift

clue_staff profile image Clue Originally published at dev.to on ・4 min read

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.

}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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"

}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

And then extending it like so:

extension ReuseIdentifying {
 static var reuseIdentifier: String {
 return String(describing: /\* er... what goes here? \*/)
 }
}
Enter fullscreen mode Exit fullscreen mode

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)
 }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.


Discussion (0)

Forem Open with the Forem app