Simplifying UICollectionView Usage With UICollectionView​DiffableDataSource

11 December 2019

Both UICollectionView and UITableView received significant updates at this year's WWDC, one of which was a new way to supply said views with the data that they display. This new data-providing functionality is afforded by two new classes - UICollectionViewDiffableDataSource and UITableViewDiffableDataSource. Most of the examples of using these new classes that Apple focussed on emphasised the benefits that apply to particularly complex collection and table views where the data being displayed is frequently changed, which in the past has been a source of headaches for developers when trying to update the view to match the new data. However, we also get worthwhile improvements to our code when using UICollectionViewDiffableDataSource for simple collection views whose data isn't frequently updated in complex ways.

Supplying Data to UICollectionView the Traditional Way

To provide a UICollectionView with data in the past we have needed an object that conforms to the UICollectionViewDataSource protocol. In the Counties sample app (available on GitHub) the collection view's data source was implemented in the MasterViewController:

extension MasterViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return countiesToDisplay.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let countyCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CountyCell", for: indexPath) as! CountyCell
        countyCell.county = countiesToDisplay[indexPath.item]
        countyCell.displayStyle = styleForTraitCollection(traitCollection)
        return countyCell
    }
}

This is fairly vanilla collection view data source code and will be familiar to anyone who has used UICollectionView. Our collection view has one section, and it contains an item for each county in the countiesToDisplay array (this array acts as the backing data store for the collection view). When the collection view requests a cell to display a county, we dequeue a cell and supply it with a county that we retrieve from the countiesToDisplay array using the supplied index path.

Let's take a closer look at that countiesToDisplay array:

var countiesToDisplay: [County] {
    guard let searchText = searchController.searchBar.text, searchText.count > 0 else {
        return County.allCounties
    }
    return spotlightSearchController.searchResults
}

The array is a computed property that returns all counties if the user is not performing a search, otherwise it returns the results of the user's search.

So far, so simple. We've been providing collection views with data this way for years. However, there are plenty of opportunities for writing bugs in code like that shown above. We are responsible for managing the backing array ourselves, and have to access it in more than one place when simply supplying data to a collection view. Each time we have to access the array and select the correct piece of data from it, we have an opportunity to introduce a bug. We can end up in a situation where the collection view's representation of our data and the actual data itself can get out of sync. It would be much better to have a single source of truth for the data to be displayed that we don't have to manage ourselves, and that can never get out of sync. This is where usage of UICollectionViewDiffableDataSource comes in.

Supplying Data to UICollectionView the New Way

Instead of writing an object that conforms to UICollectionViewDataSource ourselves, we can use UICollectionViewDiffableDataSource as our collection view's data source instead. This relieves us of the responsibility for managing the data to be displayed and simplifies our code in the process.

In order to use UICollectionViewDiffableDataSource, first we have to create section and item identifier types for the data that we wish to display. The only requirement for these types is that they conform to the Hashable protocol, which gives us plenty of scope for what to use as our identifiers. As our collection view only contains a single section, we can use a simple enum as our section identifier:

private extension MasterViewController {
    
    /// The sections displayed in the collection view.
    private enum CollectionSection: Hashable {
        case counties
    }
}

For the item identifier type, we can simply add Hashable conformance to the existing County model struct that we use to show county details in the collection view:

/*!
The struct used to represent an individual county.
*/
struct County: Hashable {
    // Implementation omitted
}

We are now ready to create our diffable data source using the identifier types that we have defined. First we add a dataSource property to our view controller to own the data source that we will create:

private var dataSource: UICollectionViewDiffableDataSource<CollectionSection, County>!

We create the data source like this:

dataSource = UICollectionViewDiffableDataSource<CollectionSection, County>(collectionView: collectionView) { [weak self] (collectionView, indexPath, county) -> UICollectionViewCell? in
    guard let self = self else { return nil }
    let countyCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CountyCell", for: indexPath) as! CountyCell
    countyCell.county = county
    countyCell.displayStyle = self.styleForTraitCollection(self.traitCollection)
    return countyCell
}

The UICollectionViewDiffableDataSource initialiser takes two parameters - our collection view, and a closure used to return UICollectionViewCells. The data source sets itself as the given collection view's dataSource during initialisation, so we don't need to do that ourselves. The code in the closure for creating cells looks very similar to our previous implementation of -[UICollectionViewDataSource collectionView:cellForItemAtIndexPath:], except for one key difference. When we conform to UICollectionViewDataSource ourselves, all we are given to retrieve the data for the cell we are to return is an IndexPath that we have to use against our own data store to retrieve the correct data. However, in this closure, we don't just receive an index path. We are also passed an instance of our item identifier, which in our case is a County model struct. This is the county we need to display in the cell; we don't need to retrieve it ourselves anymore, it is given to us automatically by UICollectionViewDiffableDataSource.

And with that, we can remove UICollectionViewDataSource conformance from our view controller as we now have a UICollectionViewDiffableDataSource instance acting as our collection view's data source instead. Lovely!

Supplying Data to the Data Source

You may have been wondering, having looked at the code for creating the diffable data source, how exactly our UICollectionViewDiffableDataSource instance knows what data to supply. UICollectionViewDiffableDataSource introduces the concept of data "snapshots" - these are simple structs containing the data that the data source is responsible for providing to the collection view. In order for the data source to provide our county data to the collection view, we must supply it with a data snapshot containing the sections and their items that we wish to be displayed in the collection view. We do this by creating a snapshot like so:

private func snapshotForCurrentState() -> NSDiffableDataSourceSnapshot<CollectionSection, County> {
    var snapshot = NSDiffableDataSourceSnapshot<CollectionSection, County>()
    snapshot.appendSections([.counties])
    if let searchText = searchController.searchBar.text, searchText.count > 0 {
        snapshot.appendItems(spotlightSearchController.searchResults)
    } else {
        snapshot.appendItems(County.allCounties)
    }
    return snapshot
}

This is essentially doing the same job as our old countiesToDisplay array - returning search results if the user is searching, or all of the counties if they are not. The crucial difference is that we create this snapshot only once, and it is then used to drive all of the collection view data source logic in UICollectionViewDiffableDataSource. The snapshot represents the data that we wish to be displayed, and UICollectionViewDiffableDataSource handles the business of marshalling this in to the information needed by UICollectionView. If the data we wish to display changes, for example when the user performs a search, we simply create a new snapshot containing the results of the search.

Having created a snapshot, we need to pass it to our diffable data source instance. This is a simple one-liner:

dataSource.apply(snapshotForCurrentState(), animatingDifferences: false)

Our collection view will be updated and display the data contained within the snapshot.

The collection view being updated with snapshots created as the user types a search query.
The collection view being updated with snapshots created as the user types a search query.

Retrieving Data from UICollectionView​DiffableDataSource

Having the data for our collection view managed entirely by a separate data source object raises an important question. If we wish to access this data ourselves, how do we go about it? We used to manage our collection view's data ourselves, so we could access it easily. How can we access it now that it is managed elsewhere? Fortunately there is a simple answer to this question.

Commonly when a user taps on an item in a collection view, we wish to respond in someway, using the data from the cell that the user tapped. Before using a diffable data source, our MasterViewController did the following:

// MARK: UICollectionViewDelegate
extension MasterViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let selectedCounty = countiesToDisplay[indexPath.item]
        showCounty(selectedCounty, animated: true)
    }
}

Since updating to use UICollectionViewDiffableDataSource, we no longer have our countiesToDisplay array. Instead, we retrieve the selected county from the data source using the -[UICollectionViewDiffableDataSource itemIdentifierForIndexPath:] method:

// MARK: UICollectionViewDelegate
extension MasterViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let selectedCounty = dataSource.itemIdentifier(for: indexPath)!
        showCounty(selectedCounty, animated: true)
    }
}

This method allows us to access the data we supplied in the most recent snapshot that we provided to the data source. Using this technique, we still have full access to the data being displayed by our collection view, even though we are no longer responsible for managing it ourselves.

Conclusion

UICollectionViewDiffableDataSource provides ample opportunity to simplify our usage of UICollectionView, even for collection views displaying relatively simple data sets. By no longer being responsible for marshalling data between the collection view and backing data store ourselves, we drastically simplify the code and eliminate entire classes of bugs associated with providing collection view data directly. Take a look at the Counties sample app on GitHub to see UICollectionViewDiffableDataSource in action, and make sure to check it out next time you build a user interface using UICollectionView.