In my last post, I looked at handling diffable data sources with different types of object data, focusing primarily on the venerable UITableView for my examples. We looked at a simple if...else
construct to swap between cell types based on the type of the item from the data source and related snapshot. While this method works with UICollectionView
objects and diffable data sources, iOS 14 introduced a brand new way to define and configure your reusable collection view cells that opens up some new options.
Setting Up the Collection View
Like last time, we’ll set up Apple
, Orange
, and EmptyData
structures to hold our sample data. These are just a couple heterogenous data structures to use with our data.
struct Apple: Hashable {
var name: String
var coreThickness: Int
}
struct Orange: Hashable {
var name: String
var peelThickness: Int
}
struct EmptyData: Hashable {
let emptyMessage = "We're sorry! The fruit stand is closed due to inclement weather!"
let emptyImage = "cloud.bold.rain.fill"
}
This time however, we want to declare and instantiate a UICollectionView
inside our view controller. Below we are identifying our possible sections as well as calling our “bad weather” data retrieval function, which will simulate an asynchronous pull of data from an API or other source to populate the sections. While many of the examples out there for diffable data sources have hard coded the section data, I think it’s more useful to have some code that mimics more typical scenarios.
/// Simple sample diffable table view to demonstrate using diffable data sources. Approximately 33% of the time, it should show "bad weather" UI instead of apples and oranges
final class DiffableCollectionViewController : UIViewController {
var collectionView: UICollectionView!
enum Section: String, CaseIterable, Hashable {
case apples = "Apples"
case oranges = "Oranges"
case empty = "Bad Weather Today!"
}
private lazy var dataSource: UICollectionViewDiffableDataSource<Section, AnyHashable> = makeDataSource()
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout())
collectionView.dataSource = dataSource
...<layout and other code>...
// Just a silly method to pretend we're getting empty data every 3rd or so call (for demo purposes every 3 days or so we get rain at the fruit stand)
let badWeatherDay = Int.random(in: 0..<3) > 0
badWeatherDay ? getData() : getEmptyData()
}
We’re also going to extend our UICollectionViewCell
code to make our life a little easier and encapsulate the actual cell label and background changes. The function below simply takes some text and a background color for the cell using the new UIListContentConfiguration
from iOS 14. This new object makes it much simpler to define collection items meant to be utilized in lists and similar scenarios.
extension UICollectionViewCell {
/// Just set up a simple cell with text in the middle
/// - Parameter label: Label
/// - Parameter relatedColor: Color associated with the data
func configure(label: String, relatedColor: UIColor) {
//Content
var content = UIListContentConfiguration.cell()
content.text = label
content.textProperties.color = .white
content.textProperties.font = UIFont.preferredFont(forTextStyle: .body)
content.textProperties.alignment = .center
contentConfiguration = content
//Background
var background = UIBackgroundConfiguration.listPlainCell()
background.cornerRadius = 8
background.backgroundColor = relatedColor
backgroundConfiguration = background
}
}
Defining and Using Pre-Configured Cells
Now that we’ve got our prep work out of the way, we can get to some of the new, exciting capabilities from iOS 14. Prior to these changes, you most likely would have used dequeueReusableCell(withReuseIdentifier:for:)
to pull and configure a collection view cell with which to work. New in iOS 14 however is the ability to create a cell registration with UICollectionView.CellRegistration
. With this we can move our configuration code out of the usual spots, refactor things, and use the registration as a variable. This generic object is defined as follows.
struct CellRegistration<Cell, Item> where Cell : UICollectionViewCell
UICollectionView.CellRegistration
expects a collection view cell with which to work and an item to use to configure the cell. This is great because it vastly simplifies some of the pain associated with managing state and pulling the correct item for the currently selected index path. If we look a little more closely at the initializers for UICollectionView.CellRegistration
we can also see that it can be defined with either a code-based handler or with a nib file (which can come in handy for some complex layout scenarios, legacy code, etc.).
public typealias Handler = (Cell, IndexPath, Item) -> Void
public init(handler: @escaping UICollectionView.CellRegistration<Cell, Item>.Handler)
public init(cellNib: UINib, handler: @escaping UICollectionView.CellRegistration<Cell, Item>.Handler)
So getting back to our example with our fruit stand, we just need to define three unique cell configurations to handle each possible data scenario.
/// Configured apple cell
/// - Returns: Cell configuration
private func appleCell() -> UICollectionView.CellRegistration<UICollectionViewCell, Apple> {
return UICollectionView.CellRegistration<UICollectionViewCell, Apple> { (cell, indexPath, item) in
cell.configure(label: "\(item.name), core thickness: \(item.coreThickness)mm", relatedColor: .systemGreen)
}
}
/// Configured orange cell
/// - Returns: Cell configuration
private func orangeCell() -> UICollectionView.CellRegistration<UICollectionViewCell, Orange> {
return UICollectionView.CellRegistration<UICollectionViewCell, Orange> { (cell, indexPath, item) in
cell.configure(label: "\(item.name), peel thickness: \(item.peelThickness)mm", relatedColor: .systemOrange)
}
}
/// Configured empty data cell
/// - Returns: Cell configuration
private func emptyCell() -> UICollectionView.CellRegistration<UICollectionViewCell, EmptyData> {
return UICollectionView.CellRegistration<UICollectionViewCell, EmptyData> { (cell, indexPath, item) in
cell.configure(label: item.emptyMessage, relatedColor: .systemRed)
}
}
Updating the Collection View with Pre-defined Cell Configurations
Since we have created our cell configurations, we’re now ready to dequeue these configurations as appropriate as we create the UICollectionViewDiffableDataSource
. We’ll do this by calling dequeueConfiguredReusableCell(using:for:item:)
with the appropriate cell configuration in the using:
parameter. As with the table view example from the last blog post, we’re just examining the item to determine which cell configuration to use.
let dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { collectionView, indexPath, item in
if let apple = item as? Apple {
//Apple
return collectionView.dequeueConfiguredReusableCell(using: self.appleCell(), for: indexPath, item: apple)
} else if let orange = item as? Orange {
//Orange
return collectionView.dequeueConfiguredReusableCell(using: self.orangeCell(), for: indexPath, item: orange)
} else if let emptyData = item as? EmptyData {
//Empty
return collectionView.dequeueConfiguredReusableCell(using: self.emptyCell(), for: indexPath, item: emptyData)
} else {
fatalError("Unknown item type")
}
}
In the above example, appleCell()
, orangeCell()
, and emptyCell()
are simply the functions defined earlier that return a UICollectionView.CellRegistration<UICollectionViewCell, Item>
instance.
Whenever we get data back from our API or data source, we can just call our updateSnapshot
function to update our UI.
/// Update the data source snapshot
/// - Parameters:
/// - apples: Apples if any
/// - oranges: Oranges if any
private func updateSnapshot(apples: [Apple], oranges: [Orange]) {
// Create a new snapshot on each load. Normally you might pull
// the existing snapshot and update it.
var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
defer {
dataSource.apply(snapshot)
}
// If we have no data, just show the empty view
guard !apples.isEmpty || !oranges.isEmpty else {
snapshot.appendSections([.empty])
snapshot.appendItems([EmptyData()], toSection: .empty)
return
}
// We have either apples or oranges, so update the snapshot with those
snapshot.appendSections([.apples, .oranges])
snapshot.appendItems(apples, toSection: .apples)
snapshot.appendItems(oranges, toSection: .oranges)
}
Setting Up Header and Footer Configurations
Just as with collection view cell registrations, iOS 14 lets us create supplementary registrations for headers and footers as well. By creating a UICollectionView.SupplementaryRegistration
with any UICollectionReusableView
, you can easily modernize how your collection view sections are defined. In the code below, we’re setting up a header for each section with the value of the section type from the snapshot.
private func configuredHeader() -> UICollectionView.SupplementaryRegistration<HeaderView> {
return UICollectionView.SupplementaryRegistration<HeaderView>(elementKind: "section-header") { (supplementaryView, title, indexPath) in
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
supplementaryView.titleLabel.text = section.rawValue
}
}
Then when we create the data source, we can call dequeueConfiguredReusableSupplementary(using:for:)
to create the supplementary view as part of its supplementary view provider closure.
dataSource.supplementaryViewProvider = { (view, kind, indexPath) in
print("\(view), \(kind), \(indexPath)")
return self.collectionView.dequeueConfiguredReusableSupplementary(using: self.configuredHeader(), for: indexPath)
}
Where To Go From Here?
With iOS 13 and especially iOS 14, Apple has totally revamped how collection views can be created with compositional layouts and diffable data sources. I’d strongly recommend checking out Implementing Modern Collection Views in the Apple developer documentation and exploring everything that has changed with collection views. While SwiftUI with the LazyHGrid
and LazyVGrid
are here and available for presenting collections, the trusty UICollectionView still has plenty of life left and may be able to meet your unique needs!
Get the source for this blog post from GitHub.