UICollectionView laid out in grid layout.

How to Use Diffable Data Source in UIKit

There’s a common requirement for modern iOS apps to display data in lists and grids of all kinds, with animations in response to the changes in that data due to user actions and/or network calls. 

These days, SwiftUI framework makes it very easy to display such dynamic lists and grids with smooth animations and custom behaviors. However, as sleek and as expressive SwiftUI may be, the reality is that there are still far too many iOS apps that rely on UIKit. This is understandable, as for some projects the costs of migration to SwiftUI may far exceed the gains. 

That being said, something as simple as displaying a list of dynamic data in UIKit can be a pretty awkward ordeal. Back in the day, iOS developers had to do a lot of manual setup to make sure that the data is updated correctly in the view, which made the whole process error prone. Fortunately, Apple has addressed these issues with the introduction of their new diffable data source—a modern way to feed data to tables and collections in UIKit. 

Don’t let the weird terminology scare you—in practice, the diffable data source is a lot easier to use than anything that came before it in UIKit, and is arguably the best way to manage data in table and collection views. In this article I’ll guide you through the setup of the diffable data source in UIKit.

What’s Diffable Data Source

In UIKit, the concept of a data source refers to an object that acts as an intermediary between your data and the table or collection view that displays it. This object manages data updates within the view and provides the cells filled with content that populate it.

When we talk about diffable data source in particular, the term “diffable” means that this data source operates by taking snapshots of the data and comparing the differences between those snapshots to apply changes to the view accordingly.

There are three main types to work with when using a diffable data source in UIKit:

  • UITableViewDiffableDataSource – As the name suggests, this data source is used in conjunction with UITableView;
  • UICollectionViewDiffableDataSource – This data source is used to fill a UICollectionView with data.
  • NSDiffableDataSourceSnapshot — This type represents a snapshot of the data, which can be applied to both data sources mentioned above.

Both of the diffable data source types work essentially in the same way, so once you’ve learned one, you’ll have no trouble implementing the other. 

The setup of diffable data source generally involves the following steps:

Once this setup is complete, you simply repeat the Step VI whenever you need to update the data in the view. Let’s go over these steps in more detail.

UICollectionViewDiffableDataSource in action—effortlessly handling updates with smooth animations. Check out the code for this video on my GitHub.

I. Define Diffable Data Source

As mentioned above, the diffable data source uses a notion of snapshots that represent the state of the data. Both—diffable data source and its snapshots—are generic types that take two type parameters to identify sections and items. Sections are used to group items in a meaningful way, while the items embody the cells of the collection view.

Schematics of the structure of diffable data source.
In diffable data source, sections contain items that display your data.

You can use any type to identify your sections and items, as long as the type conforms to Hashable protocol. It can be something as simple as Int for sections and UUID for items, or you can use your own custom types. The conformance to Hashable is necessary for the diffable data source to keep track of changes between the snapshots.

Defining a diffable data source, in this case, means parameterizing it with the types you’ll be using to identify your sections and items.

Define Sections

For simple cases it’s perfectly fine to use the Int type to represent sections, especially if your collection has only one section. However, I find it useful to explicitly define the sections for clarity.

Enumerations are great for defining sections, especially considering that enum’s are Hashable by default in Swift.

// Example with a single section
enum Section {
    case main
}

// Example with multiple sections
enum Section {
    case books, movies
    
    var title: String {
        switch self {
        case .books: return “My favorite books"
        case .movies: return “My favorite movies"
        }
    }
}

Define Items

Item identifiers can be expressed with enumerations as well. Using associated values you can pass any kind of data for an item as long as the associated value conforms to Hashable protocol. If you do use the associated values however, you have to explicitly adopt the Hashable protocol on your enumeration.

enum Item: Hashable {
    case sectionHeader(title: String)
	case book(title: String, pages: Int)
    case movie(title: String, length: Double)
    case emptyState
}

You can also use custom data types as item identifiers in a diffable data source by conforming them to the Hashable protocol, which gives you direct access to the underlying data. To make a struct conform to Hashable, ensure that all stored properties are Hashable, or implement the hash(into:) method. For most cases, hashing a UUID will suffice.

struct Movie: Identifiable {
    let id: UUID = UUID()
    var title: String
    var length: Double
}

extension Movie: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

Define Diffable Data Source and Snapshot

Once you’ve decided on the section and item types, use them to parameterize your diffable data source by writing the types inside the angle brackets.

var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

Declaring the data source at the top of the view controller as an explicitly unwrapped optional allows to skip pesky initializer implementation and rely on the familiar viewDidLoad method instead.

Tip: Use Type Aliases

Another way to parameterize the diffable data source is to use type aliases to handle the unwieldy type names, making the code easier to read and reason about.

// Diffable data source for collection view
typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>

// Snapshot for diffable data source
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>

II. Configure Collection View Layout

The cool thing about the UICollectionView is that it supports virtually any kind of layout—grids, lists, or custom compositions. The layouts for collection views are created with the UICollectionViewLayout subclasses, thus the implementation varies significantly between different layouts.

Configure List Configuration

To create a list layout for the UICollectionView use the list(using:) static method on UICollectionViewCompositionalLayout class. This method takes one argument of the UICollectionLayoutListConfiguration type that you use to configure list properties such as whether to show separators, header mode, swipe gestures, etc.

Apply the generated list layout to your UICollectionView by assigning it to the colectionViewLayout property of the collection view.

func configureCollectionViewLayout() {
    // 1. Create configuration
    var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    
    // 2. Configure properties
    configuration.showsSeparators = true
    
    // 3. Use configuration to create layout
    let layout = UICollectionViewCompositionalLayout.list(using: configuration)
    
    // 4. Apply layout to collection view
    collectionView.collectionViewLayout = layout
}

Section Headers Made Easy

When you set the headerMode property of the UICollectionLayoutListConfiguration to firstItemInSection, the configuration uses the first item in each section as the section header. This feature is extremely useful, as it lets you avoid separate registration for header views by creating headers with dedicated item identifiers instead.

configuration.headerMode = .firstItemInSection

Effortless Swipe Gestures

With the UICollectionLayoutListConfiguration it’s very easy to create swipe gestures for the list items. You simply provide a closure to any of its trailingSwipeActionsConfigurationProvider or leadingSwipeActionsConfigurationProvider properties that takes the IndexPath of the item that was swiped, and returns the UISwipeActionsConfiguration. Use the IndexPath to identify the swiped item and provide relevant UIContextualAction swipe actions. To apply the swipe actions to an item simply return the swipe actions configuration as the UISwipeActionsConfiguration type instantiated with an array of your UIContextualAction swipe actions.

configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
        
		// Ensure not to swipe first items in sections as those are used as section headers
		guard indexPath.item != 0 else { return nil }

		// Create swipe actions
        let action = UIContextualAction(style: .normal, title: "Okay") { action, view, completion in
            print("Item swiped!")
            completion(true)
        }

		// Return swipe configuration with actions
        return UISwipeActionsConfiguration(actions: [action])
    }

III. Register Reusable Cells

For optimal performance the UICollectionView uses a mechanism for reusing cells. Without delving into the nitty-gritty of how this works, the main point is that, prior to connecting the data source to the view, you must register your reusable cells.

There are multiple ways to register cells for a collection view, but I find the use of UICollectionView.CellRegistration type to be by far the most straightforward way, as it does away with unnecessary String reuse identifiers.

Parameterize Cell Registration

The UICollectionView.CellRegistration is a generic type and you need to parameterize with the type of the view for cells and the item type in your diffable data source.

var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, Item>!

Implement Cell Registration Handler

To create the UICollectionView.CellRegistration you need to pass in a closure that configures the individual cells, known as the cell registration handler. This handler takes three arguments:

  • cell – the cell to configure. Use this parameter to retrieve default list configuration for cells.
  • indexPath — the location of the cell in the collection view.
  • itemIdentifier — the item type in your diffable data source. Use it to access the data to populate the cell.
func cellRegistrationHandler(for cell: UICollectionViewListCell, at indexPath: IndexPath, item: Item) {
    
    // 1. Retrieve default list configuration
    var contentConfiguration = cell.defaultContentConfiguration()
    
    // 2. Apply content
    switch item {
    case .sectionHeader(let title): // dedicated item for section headers
        contentConfiguration.text = title
    case .book(let title, let pages):
        contentConfiguration.text = title
        contentConfiguration.secondaryText = "\(pages) pages"
    case .movie(let title, let length):
        contentConfiguration.text = title
        contentConfiguration.secondaryText = "\(length) hours"
    default:
        break
    }
    
    // 3. Apply styles
    contentConfiguration.secondaryTextProperties.color = .secondaryLabel
    
    // 4. Set configuration
    cell.contentConfiguration = contentConfiguration
    
    // 5. Optional: add accessories.
    cell.accessories = [.disclosureIndicator()]
}

Separating cell registration and its handler in this way results in clearer code, easier maintenance, and better separation of concerns, making it more future-proof.

Register Cells

Finally, initialize the cell registration object before connecting data source to the view by passing in the registration handler closure defined above.

func registerCells() {
    cellRegistration = .init(handler: cellRegistrationHandler)
}

IV. Implement Cell Provider

In context of diffable data source a cell provider is a closure that configures content and appearance of individual cells. This closure takes three arguments:

  • collectionView — The UICollectionView that data source is connected to. Used it to dequeue the cells with cell registration declared in previous step.
  • indexPath — The location, if you will, of the item within the collection view.
  • itemIdentifier — The item type used in data source.

Use these parameters to return the cell of the type that you need, for example for a list layout the return type will be UICollectionViewListCell.

Since we’re using UICollectionView.CellRegistration to register our cells, the implementation of the cell provider ends up being just one line of code.

func cellProvider(for collectionView: UICollectionView, at indexPath: IndexPath, item: Item) -> UICollectionViewCell {
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}

V. Connect Data Source to View

func connectDataSource() {
    dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: cellProvider)
}

VI. Create and Apply Snapshot

As mentioned earlier, a diffable data source uses snapshots represented with the NSDiffableDataSourceSnapshot type to fill the view with data. Whenever a new snapshot is applied, the diffable data source updates the view it’s connected to.

Typical workflow with snapshots adheres to the following pattern:

  1. Create a snapshot;
  2. Make changes to the snapshot by manipulating the sections and items;
  3. Apply the snapshot to data source.
func applySnapshot() {
    // 1. Create empty snapshot
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    
    // 2. Make changes to snapshot
    
    // Append sections
    snapshot.appendSections([.books, .movies])
    
    // These first items are section headers as per UICollectionLayoutListConfiguration.headerMode = .firstItemInSection
    snapshot.appendItems([.sectionHeader(title: Section.books.title)], toSection: .books)
    snapshot.appendItems([.sectionHeader(title: Section.movies.title)], toSection: .movies)
    
    // The rest of the items
    snapshot.appendItems(Item.sampleData.books, toSection: .books)
    snapshot.appendItems(Item.sampleData.movies, toSection: .movies)
    
    // 3. Apply the snapshot
    dataSource.apply(snapshot)
}
The UI resulting from the sample code in this article.
The UI resulting from the sample code in this article.

The NSDiffableDataSourceSnapshot provides many useful methods to work with sections and items. For example, you can delete items by retrieving the current snapshot and by calling the deleteItems(_:) method on it.

func delete(item: Item) {
    // 1. Retrieve current snapshot
    var updatedSnapshot = dataSource.snapshot()
    
    // 2. Make changes to snapshot
    // In this case, delete the item
    updatedSnapshot.deleteItems([item])

    // 3. Apply updated snapshot
    dataSource.apply(updatedSnapshot, animatingDifferences: true)
}

Passing true in the apply(_:animatingDifferences:) call will animate the updates in the view.

What’s next

Now that all of the functionality is ready, all that is left to do is to call the functions we declared above at the right time in the viewDidLoad.

func viewDidLoad() {
    super.viewDidLoad()
    configureCollectionViewLayout()
    registerCells()
    connectDataSource()
    applySnapshot()
}

Then, whenever you need to display changes in data, simply repeat pattern discussed in the previous step: create a snapshot, make changes to it, apply it.

Custom Content Configurations

The default list configuration discussed in this guide will fit majority of simple use cases, but sometimes there’s need to display more complex views in your cells, such as text fields in forms, or specialized views such as maps.

Custom content views inside UICollectionView cells with the help of UIContentView.
The items in the collection don’t have to be the same. In fact, with the help of UIContentView protocol you can implement any content view for your cells.

Wrap Up

I hope that with this guide succeeded in demystifying the diffable data sources for you, and provided you with some useful insights. Happy diffing!

Discover more from Dudee

Subscribe now to keep reading and get access to the full archive.

Continue reading