Ever since the evolution of iOS SDK, it has been the responsibility of the data source, to provide the table-views and collection-views with the data they need to create, configure and display its cells and supplementary views. For this, we are required to implement the necessary protocol (UITableViewDataSource or UICollectionViewDataSource) methods and sync our updated data-model with the data source properly to avoid any synchronization bugs. This approach has been used for a decade now but still, it has quirks and fallout which could sometimes give you a headache.

With iOS 13.0, Apple provided a couple of really cool API’s for both table-views and collection-views. One of these cool features is Diffable Data Sources. In today’s article, I will show you how to use UICollectionViewDiffableDataSource to drive your collection-views. By the end of this article, you will know exactly how to use Diffable Data Sources and what their caveats are. But before that let’s discuss and understand why Apple had to provide a brand new API for data sources. Let’s dive in!

Current state-of-the-art:

At present, we interact with the UI data sources for both table-views and collection-views in quite a similar way. Firstly,  we request the number of sections it contains, then the number of items each section contains and as the content renders, we ask for the item’s view, or in other words, the cell.

Let’s say you are using a UICollectionView to show certain items to the users. Whenever an application receives some data, we need to update the view with the most recent information. This is not always easy to do. The simplest way is to reload the view completely, just by calling the reloadData() method on the collection-view. But this caused two problems:
reloadData() uses much more computational power than the update might require, which is not right.
– With reloadData(), we lose some nice animations for the updates, which is not good for user experience.

The alternative to this is performBatchUpdates(_:completion:), where the data model updates can be applied to the collection-view in one single animated operation, like the sample code shown below:

collectionView.performBatchUpdates({ () -> Void in
   self.collectionView.deleteItems(at: subtractedIndexPaths)
   self.collectionView.moveSection(currentSectionIndex, toSection: newSectionIndex)
   self.collectionView.insertItems(at: addedIndexPaths)
}, completion: nil)

It’s actually pretty hard to make sure all the sections and rows are added / updated / deleted in the correctly order. Any mistake and you get a crash, like the one shown below:

This happens because the data items, for the UI layer’s data source, doesn’t match with the controller’s data source.
Apple recognized this problem and even gave a presentation at WWDC 2018, where they explained how to deal with such situations elegantly. But still, the approach is very error-prone. Why? Because it makes us — the developers, handle all of the updates and enforce it onto the UI layer’s data source correctly. And as you might have experienced —  it is not an easy task.

*  *  *

A new approach: Diffable Data Source

At WWDC 2019, Apple introduced Diffable Data Source, a new form of data source which simplifies the update and management of the view by using automatic diffing to apply any changes. This improved data source mechanism completely avoids synchronization related bugs, exceptions, and crashes. It comes for UIKit‘s UITableView (UITableViewDiffableDataSource), UICollectionView(UICollectionViewDiffableDataSource) and AppKit‘s NSCollectionView (NSCollectionViewDiffableDataSource).

Diffable Data Source classes allow us to define data sources in terms of snapshots that represent the current state of the underlying data-models.
Then on every update, the data source object compares the newly received snapshot with the old one and automatically applies any insertions, deletions, and reordering of its contents, with state-of-the-art animation.

So, let’s discuss this new approach with an example.

*  *  *

Getting Started:

The demo app that we are creating will contain a food item list (implemented using a UICollectionView) having a simple vertical scrollable behavior and a search bar to filter out a dish from the list.

To get started, download the starter project and open DiffableDataSourceExample.xcodeproj with Xcode. The starter project contains the models and a Main.storyboard with the scene for FoodListingViewController, which contains the collection-view that’ll be used to show food listing.


*  *  *

Setting up the data source:

The diffable data sources use type-safe identifiers to identify its sections and items, instead of IndexPaths. The UICollectionViewDiffableDataSource class uses Generics at its very core and requires a type for the sections and items it contains. The only requirement for the type is that it has to be unique, and for this, it should conform to Hashable.

To take things slowly, we will have just one section for our collection-view. We can define a section type to use with diffable data sources like this:

enum Section {
   case main
}

As enums conform to Hashable by default, we can use this type, showing just one section in the collection-view, uniquely identified by just the enum’s main case.

As for the item’s type, all we need to do is make the Dish struct conform to Hashable, like this: struct Dish: Hashable.  All other requirements for Hashable will be auto-synthesized.

Now, for creating data source object, add this stored property in the FoodListingViewController:

private var dataSource: UICollectionViewDiffableDataSource<Section, Dish>!

and update the controller’s configureDataSource() method to this:

extension FoodListingViewController {
   func configureDataSource() {
      dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
        (collectionView: UICollectionView, indexPath: IndexPath, dish: Dish) -> UICollectionViewCell? in
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DishCollectionViewCell.identifier, 
                                                                for: indexPath) as? DishCollectionViewCell
            else { fatalError("Cannot create new cell") }
            cell.setup(using: dish)
            return cell
      }
   }
}

Here, we have provided value to our data source. We created a UICollectionViewDiffableDataSource object and provided a closure which will be called in order to get the view’s for the collection-view’s items (just like UICollectionViewDataSource‘s collectionView(_:cellForItemAt:)).


*  *  *

Add / Remove / update items using the data source:

We have already done creating our data source. But as you might have guessed, we haven’t told the data source how many items or sections we need to show ??

For that, update the performQuery(with filter: String?) method to this:

extension FoodListingViewController {
   func performQuery(with filter: String?) {
      var dishes = self.dishes
      if let filter = filter, !filter.isEmpty {
          dishes = self.dishes.filter({ $0.name.contains(filter) }).sorted { $0.name < $1.name }
      }
      var snapshot = NSDiffableDataSourceSnapshot<Section, Dish>()
      snapshot.appendSections([.main])
      snapshot.appendItems(dishes)
      dataSource.apply(snapshot, animatingDifferences: true)
   }
}

Here, we have first filter out and sorted the items to be shown, as per the text in the UISearchBar(which will be empty at start). Then created a NSDiffableDataSourceSnapshot object and appended the section and items for that section. This snapshot is then applied to the data source, which automatically compares the new snapshot to the old snapshot it has and it will automatically apply any insertions, deletions, and reordering of its contents.

This way, the onus which was earlier on the developer, is removed to find the updates to be done and make them in a performBatchUpdates(_:completion:) method. Also, this is way more simple to use and easy to understand than the performBatchUpdates(_:completion:).

Now, if you run the app, you will see the dishes vertically scrollable and searching for a dish filter out the items with the nice system-provided animations.

NOTE: As seen in the above code snippet, the command snapshot.appendItems(items) add in the items to the last added section. In case to add items in a particular section, use snapshot.appendItems(items, toSection: section) method, passing in the section.

*  *  *

Categorizing the items into different sections:

Until now, we have only one section showing all the dishes. Now let’s separate the dishes into Veg and Non-Veg. A Supplementary view is used for showing headers/footers in a UICollectionView. To create one, open the Main.storyboard, select your collection-view in the ViewController Scene and enable Section Header in the Attributes Inspector.

Now, create a simple Supplementary view, having just a UILabel to show the section type.

Also, create a class for the Supplementary view and link the UILabel to its outlet. This will be used to dynamically update the text as per the section type.

final class CollectionReusableHeaderView: UICollectionReusableView {
    static let identifier = "collectionReusableHeaderView"
    @IBOutlet weak private(set) var titleLabel: UILabel!
}

Now that we have created the necessary UI and class for the header view, lets update the Section into this:

 
enum Section { 
   case veg
   case nonVeg
} 

As we earlier passed in a closure to the data source which was used to create the view’s for the collection-view’s items, we would need to pass in a closure which will be used to create the Supplementary View.

Add this in the earlier implementation of configureDataSource():

 
extension FoodListingViewController { 
   func configureDataSource() { 
      ...
      dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in
         guard kind == UICollectionView.elementKindSectionHeader  else { return nil }
         let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
                                                                          withReuseIdentifier: CollectionReusableHeaderView.identifier,
                                                                          for: indexPath) as? CollectionReusableHeaderVid()
         headerView?.titleLabel.text = (indexPath.section == 1) ? "NON-VEG" : "VEG"
         return headerView
      }
   } 
} 

Here, we are have simply created a CollectionReusableHeaderView object, updated it’s UI as per the section type and returned it. The collection-view’s layout will itself be calling the closure in order to get the supplementary views.

But wait, we have just provided a way to create the supplementary views, but still haven’t filtered the dishes into Veg or Non-Veg. Let’s do that by updating the performQuery(with filter: String?) method.

extension FoodListingViewController {
    func performQuery(with filter: String?) {
        var dishes = self.dishes
        if let filter = filter, !filter.isEmpty {
            dishes = self.dishes.filter({ $0.name.contains(filter) }).sorted { $0.name < $1.name }
        }
        var snapshot = NSDiffableDataSourceSnapshot<Section, Dish>()
        let vegDishes = dishes.filter({ $0.isVeg })
        if !vegDishes.isEmpty {
            snapshot.appendSections([.veg])
            snapshot.appendItems(vegDishes, toSection: .veg)
        }
        let nonVegDishes = dishes.filter({ !$0.isVeg })
        if !nonVegDishes.isEmpty {
            snapshot.appendSections([.nonVeg])
            snapshot.appendItems(nonVegDishes, toSection: .nonVeg)
        }
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

You can find the final project here. Now, if you run the app, you will see the items be separated into two sections. And searching for a dish item applies the view-model updates with the soothing system-provided animations.


*  *  *

Conclusion

Diffable data sources is a huge leap forward in terms of how easy both table views and collection views are to work with — and how stable the implementations we build on top of them are likely to become. By providing a more declarative API that moves much of the complexity of dealing with UI state into UIKit itself, a huge class of mistakes and bugs can be avoided — most likely resulting in fewer crashes, and better-performing apps.

If you have any questions, please don’t hesitate to ask me in the comments!

 

Share this: