How to use UITableViewDiffableDataSource with UITableView in UIKit

How to Use Diffable Data Source with UITableView

You might be surprised to know just how many of our everyday iOS apps rely on simple tables to display all sorts of UIs — the usual feeds and tables of data, but also settings lists and all kinds of forms.

Considering that UITableView has beed around for a while, it’s no wonder it’s so commonly used for tables in UIKit apps. The UITableView comes with tons of powerful methods for managing tables, such as deleting cells, reordering cells, and so on. However, it truly shines when it’s coupled with the UITableViewDiffableDataSource type. 

You see, it’s easy to display static data in UITableView. But if you want to display dynamic data—something that changes throughout the lifecycle of the table view—you’ll have to jump through a lot of hoops for the whole thing work smoothly.

Fortunately, there’s a solution—diffable data source—specifically the UITableViewDiffableDataSource type I mention above. In this article I’ll show you how to use it with UITableView effectively.

What’s Diffable Data Source

Diffable data source is a layer between your data and the table view, that takes snapshots of the state of your UI, and applies the changes to it based on the differences between those snapshots. With diffable data source it’s super easy to implement tables that support gestures and smooth animations.

Without further ado, let’s begin.

See the implementation of the UI shown on this video in my sample Xcode project on GitHub.

I. Define Diffable Data Source

The first thing to do before displaying data in the tables is to actually define it. As a generic type, the UITableViewDiffableDataSource requires two type parameters that will identify the data in your table view. One type parameter  identifies the sections of your table and the other the items in the sections. You can use any types for section and item identifiers, with the only requirement being that both must conform to the Hashable protocol.

I find enumerations exceptionally useful for describing the sections and items in tables, particularly because in Swift enumerations are Hashable by default. So usually I’d declare my sections and items as separate types either nested within my view controller or globally if I plan to use the same types in other places.

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

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

Once you know exactly what data types you work with, use them to parametrize your diffable data source:

var dataSource: UITableViewDiffableDataSource<Section, Item>!

II. Register Reusable Cells for UITableView

To instruct the table view on how to create cells, you need to register them. The approach depends on whether you’re building your UI programmatically or using a storyboard, but in both cases, a unique identifier is required for cell registration. It’s essential that this identifier remains consistent wherever it is used, so defining it as a constant source of truth is certainly a good idea:

let cellReuseIdentifier = “Cell"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)

Register Cell for UITableView in Storyboard

If you created your UITableViewController in storyboard, you can still register cells programmatically as shown above, or you can do so in your storyboard directly. In the Interface Builder, select the prototype cell of your table view and switch to Attributes Inspector (⌘+⌥+5). In the Attributes Inspector navigate to the Identifier field and enter your reuse identifier. Be sure to use the same identifier in your code and storyboard for the whole thing to work.

Registering cells for UITableView in Storyboard in Interface Builder in Xcode.
Registering cells for UITableView in Storyboard in Interface Builder in Xcode.

III. Implement Cell Provider for UITableViewDiffableDataSource

To create an instance of UITableViewDiffableDataSource you need to implement a cell provider. A cell provider is a closure that you use to configure the content and the appearance of individual cells. This closure takes three arguments to return the view for the cell:

The easiest way to implement the cell provider is to create a cell by calling the dequeueReusableCell(withIdentifier:for:) method on the table view, passing in the cell reuse identifier you used to registered your cells and the indexPath parameter that closure passes on.

let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)

Create Cell Content Configuration

Unless you’re subclassing the UITableViewCell, customizing the created cell is a breeze using the default list configuration. To retrieve this configuration call the defaultContentConfiguration() on the created cell. The return type of this call is the UIListContentConfiguration that will fit most of the use cases, such as displaying lists with images and secondary text.

var contentConfig: UIListContentConfiguration = cell.defaultContentConfiguration()

Customize Cell Content and Appearance

Use the retrieved configuration to provide content and change appearance of your cell. Once done, apply the configuration by assigning it to contentConfiguration property of the UITableViewCell that you created.

// Configure content
contentConfig.text = title
contentConfig.secondaryText = "\(pages) pages"

// Configure cell appearance
contentConfig.secondaryTextProperties.color = .secondaryLabel
    
// Apply the content configuration to the cell
cell.contentConfiguration = contentConfig

Cell Provider for UITableView

Usually you’ll see the cell provider implemented as a trailing closure during data source initialization, however I find it useful to declare the cell provider separately for easier code maintenance.

func cellProvider(for tableView: UITableView, at indexPath: IndexPath, item: Item) -> UITableViewCell {
    
    // Dequeue reusable cells using `tableView` and `indexPath` parameters
    let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
    
    // Generate default content configuration for cell
    var contentConfig = cell.defaultContentConfiguration()
    
    // Configure cell content based on the item
    switch item {
    case .book(let title, let pages):
        contentConfig.text = title
        contentConfig.secondaryText = "\(pages) pages"
    case .movie(let title, let length):
        contentConfig.text = title
        contentConfig.secondaryText = "\(length) hours"
    }
    
    // Configure cell appearance
    contentConfig.secondaryTextProperties.color = .secondaryLabel
    
    // Apply the content configuration to the cell
    cell.contentConfiguration = contentConfig
    
    // Return the configured cell
    return cell
}

Custom Views for Cells in UITableView

IV. Connect Diffable Data Source to UITableView

The link between diffable data source and UITableView is established during initialization of the former. To create an instance of UITableViewDiffableDataSource use the init(tableView:cellProvider:) initializer, passing in the table view you want to connect to and the cell provider implemented in the Step III of this article:

dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: cellProvider)

V. Create and Apply Snapshot

To display data in a table view using diffable data source you need to create a snapshot of your data with sections and items and then apply that snapshot to your data source.

First, create a snapshot of the NSDiffableDataSourceSnapshot generic type, parameterizing it with the same type parameters you used as section and item identifiers for your diffable data source.

Then, append sections by calling appendSections(_:) on the snapshot, and items to sections with the appendItems(_:toSection:) call on the same snapshot.

Once ready, simply apply the snapshot by calling the apply(_:animatingDifferences:) method on the diffable data source, passing in the created snapshot and a boolean indicating whether to animate the changes in the table view. 

func applySnapshot() {
    // Create empty snapshot
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    
    // Append sections
    snapshot.appendSections([.books, .movies])
    
    // Append items
    for books in model.books {
        snapshot.appendItems([books], toSection: .books)
    }
    snapshot.appendItems(model.books, toSection: .books)
    snapshot.appendItems(model.movies, toSection: .movies)
    
    // Apply snapshot
    dataSource.apply(snapshot, animatingDifferences: true)
}

What’s Next

At this point the diffable data source is set. Whenever you need to change the data in the table, simply create a snapshot and apply it. But before we wrap up, let me leave you with a few more tips on how to use table view with diffable data source.

Adopt UITableViewDelegate protocol

It’s much easier to implement table views inside a dedicated UITableViewController. But if you must use UITableView inside a different view controller, make sure your view controller adopts UITableViewDelegate protocol and is assigned to the delegate property of your table view. This ensures that all of the UITableViewDelegate methods (that you will undoubtedly implement in the future) will work as expected. 

// UITableViewController
class TableViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
    }
}

// Any other view controller
class ViewController: UIViewController, UITableViewDelegate {
    
    var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
    }
}

Add Section Headers to UITableView

Adding section headers to table view is similar to the cell provider implementation from earlier, in the sense that you also need a reuse identifier for the header, and you need to register the reusable header with your table view. 

// It's a good idea to declare the reuse identifier explicitly
let headerReuseIdentifier = "Header"

// Register your header view
tableView.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: headerReuseIdentifier)

In order to display the section headers in the table view implement two of the  UITableViewDelegate protocol methods, namely the tableView(_:viewForHeaderInSection:) to provide the view for the header and the tableView(_:heightForHeaderInSection:) to specify the height of it.

override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    
    // Make sure there is a header view, otherwise bail
    guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerReuseIdentifier) else { return nil }
    
    // Generate default header view configuration
    var contentConfig = headerView.defaultContentConfiguration()
    
    // Configure header content
    contentConfig.text = dataSource.sectionIdentifier(for: section)?.title
    
    // Assign header content configuration
    headerView.contentConfiguration = contentConfig
    
    // Return the header view
    return headerView
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 40.0
}

Voila! You got yourself some section headers in table view!

Delete Rows in UITableView Using Diffable Data Source

When implementing the snapshot we already covered how to add new items to the table. Another common feature of the tables is the ability to remove items from it. While the data architecture dictates how you handle data in your model, let me to show you a way to delete items in the table using diffable data source. 

For example, consider the swipe gestures to the table rows. It’s conventional to place delete swipe actions on the right side of the row, so for this we’ll need to implement the tableView(_: trailingSwipeActionsConfigurationForRowAt:) method of the UITableViewDelegate protocol. This method, similar to other UITableViewDelegate methods, exposes the IndexPath of the row that was swiped. You can use this IndexPath to identify the item that needs to be removed by calling itemIdentifier(for:) method on your diffable data source.

override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    
    // Create delete swipe action
    let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, completion in
        self.deleteItem(at: indexPath)
        completion(true)
    }
    
    // Return swipe actions
    return UISwipeActionsConfiguration(actions: [deleteAction])
}

func deleteItem(at indexPath: IndexPath) {
    // Check if the item exists
    guard let itemToDelete = dataSource.itemIdentifier(for: indexPath) else { return }
    
    // Get the current snapshot
    var snapshot = dataSource.snapshot()
    
    // Delete the item
    snapshot.deleteItems([itemToDelete])
    
    // Apply the udpated snapshot
    dataSource.apply(snapshot, animatingDifferences: true)
}
You can find the code for the UI on this video in my sample project on GitHub.

Notice, this time around we did not create an empty snapshot. Instead we retrieved the current snapshot of the data by calling the snapshot() method on the data source. Then deleting existing items with the snapshot is the same as appending new ones—just pass in the array of items to delete (in this case the array contains only one item) in the deleteItems() call on the snapshot. Finally, make sure to apply your updated snapshot once finished.

Wrap Up

I hope you have enjoyed reading this article and that the guidance provided here will serve you well. Happy diffing!

Discover more from Dudee

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

Continue reading