Registering Collection View Cells in iOS 14

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.

Published by Mark Thormann

As a software developer and architect, I enjoy using technology to craft solutions to business problems, focusing on all aspects of native iOS and Android mobile development as well as application architecture, automation. and many other areas of expertise. I'm currently working at one of the leading career-related companies in the United States, using mobile applications to help connect job seekers in the technology industry to the employment which they need.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: