iOS
Collection View Content Configuration in iOS 14
Matt Neuburg
Written on January 6, 2021

In a series of earlier articles, I introduced the iOS 14 cell content configuration architecture. My examples consisted entirely of table view cells, but I was careful to point out that exactly the same thing applies to collection view cells because UICollectionViewCell has contentConfiguration
and backgroundConfiguration
properties, just like UITableViewCell.
A collection view cell, unlike a table view cell, does not dispense a defaultContentConfiguration
. But nothing stops you from applying your own content configuration. And the content configuration that you apply can be a UIListContentConfiguration, which comes with text
and image
properties. This can save you a lot of time and can simplify your code and preparation considerably.
In this example, my goal is to portray the three Pep Boys in a vertically scrolling collection view. I already have appropriately named images in the asset catalog. This is the complete code needed:
class ViewController: UICollectionViewController {
let cellID = "cell"
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: self.cellID)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
let pep = ["manny","moe","jack"]
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellID, for: indexPath)
var config = UIListContentConfiguration.cell()
config.image = UIImage(named: self.pep[indexPath.item])
cell.contentConfiguration = config
return cell
}
}
What’s remarkable about that code is that my registered collection view cell type is a plain vanilla UICollectionViewCell. It has no built-in imageView
property. In the past, you would have had to add the image view yourself. For example, you might design the prototype cell in the storyboard, or you might add an image view as a subview of the cell’s contentView
in code; you might also declare a UICollectionViewCell subclass to give yourself an instance property pointing at the image view. But we’re not doing any of that here! We just create the list content configuration and hand it to the cell; the configuration creates the content view, containing the image view, and the cell makes that its own contentView
.
Registration Objects
Are you sick and tired of the cell reuse identifier dance? You can see me doing it in the preceding code. I have to use the cell reuse identifier twice — once to register the cell class, and again to dequeue the cell. I don’t want to hard-code this string in two places, which runs the risk of making a typo, so I’ve declared a constant instance property.
New in iOS 14, you can avoid using a reuse identifier string by using a cell registration object instead. This object embodies the cell configuration code in a function that you supply; this function is handed the cell by reference, so you just configure it — you don’t return it. Meanwhile, back in the old cell configuration function, where you were calling dequeueReusableCell
, you call dequeueConfiguredReusableCell
instead — and just return the cell.
Here’s a rewrite of the preceding code that shows you what I mean:
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
var config = UIListContentConfiguration.cell()
config.image = UIImage(named: name)
cell.contentConfiguration = config
}
let pep = ["manny","moe","jack"]
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
collectionView.dequeueConfiguredReusableCell(using: cellreg, for: indexPath, item: self.pep[indexPath.item])
}
}
In that code, the cellID
instance property has been replaced by a cellreg
instance property, which is a CellRegistration object. It’s a generic, so we have to specify the cell class; we also specify the type of data that will be used to configure the cell. If we were getting our cell from a nib, we would also include the nib in the initializer. And last but not least, the initializer includes a function.
Focus your attention on that function. It receives three parameters: the cell, the index path, and the data, which I’ve called name
. In the body of the function, I have a reference to the cell, so I just go ahead and apply the content configuration to it, just as I was doing before. Observe that I didn’t use the index path for anything! I’m receiving the data directly — the name of the pep boy — and that’s all I need in order to configure this cell.
Meanwhile, back in the cellForItemAt
data source method, what happens? I simply call dequeueConfiguredReusableCell
, passing the data into it, and immediately return the cell. It’s a one-liner, so I don’t even need the return
keyword. The first parameter is the cell registration object, so this serves to hook up the dequeued cell with the function that will configure it.
What’s the advantage of this approach? Well, the way I’ve done it, not much. The code is shorter by one line. But I’ve also eliminated the cell reuse identifier string, so there’s a gain in clarity and compactness. Still, the real value of this approach doesn’t quite emerge unless you’re also using a diffable data source, so let me switch to that architecture and demonstrate.
Diffable Data Source
I do hope you’re using diffable data sources, especially for collection views, because a number of new iOS 14 features depend upon them. I’m not going to go into diffable data sources in depth here; this is not a new feature (it was introduced in iOS 13). I’ll just rewrite the preceding code yet again, to use a diffable data source. Actually, I’m going to rewrite it twice. Here’s my first pass:
class ViewController: UICollectionViewController {
let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
var config = UIListContentConfiguration.cell()
config.image = UIImage(named: name)
cell.contentConfiguration = config
}
lazy var datasource = UICollectionViewDiffableDataSource<String,String>(collectionView: collectionView) { collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(using: self.cellreg, for: indexPath, item: item)
}
override func viewDidLoad() {
super.viewDidLoad()
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["pepboys"])
let pep = ["manny","moe","jack"]
snap.appendItems(pep)
self.datasource.apply(snap, animatingDifferences: false)
}
}
That demonstrates the basics of a diffable data source. The data source methods numberOfItemsInSection
and cellForRowAt
are gone; the data source’s data answers the question of how many items there are, and the data source itself is initialized with a cell provider function that essentially does the same work that cellForRowAt
was doing. Now that we have a cell registration object, that work consists of returning a configured cell from the call to dequeueConfiguredReusableCell
.
At this point, you should be thinking: “Wait, why is cellreg
an instance property?” Very good, that’s the right question! Previously, we needed cellreg
to be an instance property because cellForRowAt
was going to be called many times, but only one cell registration object was needed. We have kept that architecture because the data source initializer’s cell provider function, too, will be called many times. But if we move the initialization of the data source into viewDidLoad
, we can turn cellreg
into a local variable. Here’s a rewrite that does that:
class ViewController: UICollectionViewController {
var datasource : UICollectionViewDiffableDataSource<String,String>!
override func viewDidLoad() {
super.viewDidLoad()
let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
var config = UIListContentConfiguration.cell()
config.image = UIImage(named: name)
cell.contentConfiguration = config
}
self.datasource = UICollectionViewDiffableDataSource<String,String>(collectionView: collectionView) { collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(using: cellreg, for: indexPath, item: item)
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["pepboys"])
let pep = ["manny","moe","jack"]
snap.appendItems(pep)
self.datasource.apply(snap, animatingDifferences: false)
}
}
That’s really the kind of thing you’re expected to do, and it shows why cell registration objects are cool. We no longer have any extra instance properties: there’s no instance property for the cell reuse identifier string, and there’s no instance property for the cell registration object. Instead, everything happens locally in the course of viewDidLoad
. We create the cell registration object as a local variable. We initialize the diffable data source with a cell provider function that can see the cell registration object directly (because it’s a closure). And we populate the diffable data source with its initial data.
The New Dispensation
What we’ve evolved over the course of this article is basically a whole new way of writing collection view code:
-
We use a diffable data source. (Okay, that part is not completely new).
-
There is no reuse identifier string; instead, we use a cell registration object. (New in iOS 14.)
-
In the cell registration object’s cell configuration function, we’re using a list content configuration object. (New in iOS 14.)
-
In the diffable data source’s cell provider function, we dequeue with
dequeueConfiguredReusableCell
, taking advantage of the cell registration object. (New in iOS 14.) -
Finally, we construct the diffable data source’s initial data and apply it through a snapshot. (Not new.)
You’ll probably want to start using that pattern in your own code going forward.
I should also point out, just in case it isn’t completely obvious, that just about everything in viewDidLoad
can now be moved off into a separate object. What I like to do is subclass UICollectionViewDiffableDataSource and move the subclass declaration off into another file:
class MyCollectionViewDataSource : UICollectionViewDiffableDataSource<String,String> {
init(_ collectionView: UICollectionView) {
let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
var config = UIListContentConfiguration.cell()
config.image = UIImage(named: name)
cell.contentConfiguration = config
}
super.init(collectionView: collectionView) { collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(using: cellreg, for: indexPath, item: item)
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["pepboys"])
let pep = ["manny","moe","jack"]
snap.appendItems(pep)
self.apply(snap, animatingDifferences: false)
}
}
That is a self-configuring diffable data source. The outcome is that our view controller code now looks like this:
class ViewController4: UICollectionViewController {
lazy var datasource = MyCollectionViewDataSource(self.collectionView)
override func viewDidLoad() {
super.viewDidLoad()
_ = self.datasource // tickle the lazy var
}
}
Smaller view controllers, I need hardly say, are better.
(It’s a pity that there are no cell registration objects for UITableView cells. I can’t explain why Apple failed to provide them. It’s almost enough to make one subscribe to the current paranoid theory that Apple is planning to eliminate table views altogether.)