Last week at WWDC Apple announced a lot of exciting new features and frameworks. One of my favorites was the introduction of diffable data sources for UITableView and UICollectionView. Let us dive right in and see how it can be used in combination with UICollectionView.

What is a diffable data source?

The data source of a collection view is responsible for providing the collection view with the data that it needs to display and creates and configures its cells and supplementary views.

Whenever your apps data changes, for example when new data has come in from a remote server the collection view needs to update its views. This can be done by calling reloadData() or performBatchUpdates(_:completion:) on the collection view. You typically want to use performBatchUpdates instead of reloadData as the latter reloads all the data and performBatchUpdates lets you update only the parts of the collection view that should actually change in a single animated operation. This leads to a much better user experience.

To make this work we need to get the difference between the new data and the current state of the collection view and make sure the changes are applied in the right order in performBatchUpdates. Because this is quite complex it is a common source of bugs.

The new UICollectionViewDiffableDataSource takes care of all that for you. It does so through a new class called NSDiffableDataSourceSnapshot, which represents the state of the collection view. To display data in the collection view we simply create a snapshot with the updated data and provide it to the data source through calling its apply(_:animatingDifferences:) method. It will then get the current snapshot which represents the current state of the collection view, diff and update the UI.

Setting up the data source

Instead of indexPaths a diffable data source uses type safe identifiers to identify its sections and items. The only requirement for identifiers is that they must be unique and conform to Hashable. Lets us look at an example.

    enum Section {
      case startingFive
      case bench
    }

    struct BasketballPlayer: Hashable {
        let identifier: UUID = UUID()
        let name: String
        let number: Int

        func hash(into hasher: inout Hasher) {
            return hasher.combine(identifier)
        }

        static func == (lhs: BasketballPlayer, rhs: BasketballPlayer) -> Bool {
            return lhs.identifier == rhs.identifier
        }
    }

Here we create an enum called Section which defines the sections in the collection view and a struct BasketballPlayer which represents the data we are going to display in the collection view. We use an UUID for calculating the hash value. For Section we do not need to do anything as enums automatically conform to the Hashable protocol.

Next we need to create an instance of the data source. When we do that we need to tell it which types to use for the section and item identifiers and provide it with a cell provider closure which will be used to create and configure the cells.

 extension RosterViewContoller {
        
        func setupDataSource() {
            dataSource = UICollectionViewDiffableDataSource <Section, BasketballPlayer>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, player: BasketballPlayer) -> UICollectionViewCell? in
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "player-cell-identifer", for: indexPath) as! PlayerCell
                cell.nameLabel.text = player.name
                return cell
            }
        }
    }

Then to display data all we need to do is create a snapshot, configure it with the data and apply. We do this in a helper method so that we have a single entry point to update the data source, which makes it easy to reason about the data flow in this particular view controller. In viewdidLoad() we call this method to load the initial data into the collection view. Every time we get notified that the data has changed we call the same method to update the collection view.

final class RosterViewController: UICollectionViewControler, TeamDataProviderDelegate {

  private let dataProvider: TeamDataProvider()
  private var dataSource: UICollectionViewDiffableDataSource<Section, BasketballPlayer>!

  override func viewDidLoad() {
    super.viewDidLoad()
    
    setupDataSource()
    dataProvider.delegate = self
    updateCollectionView(animated: false)
  }

  func TeamDataProviderDidUpdate(_ provider: TeamDataProvider) {
    updateCollectionView(animated: true)
  }

  func updateCollectionView(animated: Bool) {
    let snapshot = NSDiffableDataSourceSnapshot<Section, BasketballPlayer>()
    snapshot.appendSections([.startingFive, .bench ])
    snapshot.appendItems(dataProvider.startingFive, toSection: .startingFive)
    snapshot.appendItems(dataProvider.benchPlayers, toSection: .bench)
    dataSource.apply(snapshot, animatingDifferences: animated)
  }
}

Extras

When you are working with a large set of data diffing can take time. If you run into a situation where diffing takes to much time you can decide to call the apply function from a background queue. The data source will then diff on the background queue and return to the main queue to update the collection view. The only rule here is to be consistent and always call the apply function from the same queue.

Finally, if you are using Core Data you can now use diffable data source together with the new NSFetchedResultsControllerDelegate method that returns a snapshot to easily update your UI when the data changes.

Conclusion

As you can see UICollectionDiffableDataSource makes it much easier to work with UICollectionView. It is good to know there are also equivalent classes available for UITableView and NSCollectionView. Watch 220 - Advances in UI Data sources for more info on this topic. Go try it out!

Contact me on Twitter @kairadiagne if you have any questions, comments or feedback.