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.
I actually wrote a whole article where I discuss the ins-and-outs of the diffable data source in UIKit, so you’re welcome to check it out if you’d like to delve deeper. In that article I established a certain setup workflow for diffable data sources, which I’ll be using here as well. Here’s the workflow I use to implement diffable data source for UITableView:
- Define diffable data source
- Register reusable cells
- Implement cell provider
- Connect diffable data source to view
- Create and apply snapshot
If you’d like to see the diffable data source in action, you’re welcome to check out the sample Xcode project I created for this article, where I demonstrate the use of diffable data source with both UITableView and UICollectionView types.
Without further ado, let’s begin.
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"
Register Cell for UITableView Programmatically
To register cells for UITableView programmatically call its register(_:forCellReuseIdentifier:) method, passing in the class of the cell view—which is either the UITableViewCell class or a subclass of it—and your reuse identifier:
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.

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:
tableView– theUITableViewto which you connect the data source;indexPath— the location of the cell in the table view used to dequeue the view for the cell;itemIdentifier— the identifier for the item in the cell. The type here is the one you used as you item identifier type when parameterizing your data source.
The returned cell is of the UITableViewCell type or a relevant subclass of it, depending on the class you registered in Step II above.
Creating Cell for UITableView
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
The approach described above is the simplest way to provide cells and meets the customization needs of most common cases. However, if you want to create custom views for your cells, you can either subclass UITableViewCell or, even better, build custom views and configurations using the UIContentView protocol. For a deeper dive into using UIContentView, check out my article on creating custom cell views.
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)
You can implement your cell provider inline here as a trailing closure of the initializer, however I recommend encapsulating the cell provider in a function for cleaner code.
The data source will then wire itself up as the data source of the table view, so you don’t need to set the dataSource property of your UITableView. In fact, after connecting UITableViewDiffableDataSource to UITableView you can’t change the dataSource property of that table view. The only way around it is to create a new table view with a new data source.
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
}
}
The UITableViewController conforms to UITableViewDelegate protocol by default, but the delegate property of the table view still needs to be assigned manually.
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
}
The tableView(_:heightForHeaderInSection:) method returns the height of section headers as a CGFloat value. You can use this method to calculate the desired height, but I found 40.0 points to be sufficient.
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)
}
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
This wraps up my guide on the use of diffable data source with the table views. I tried to keep this article to the point and the implementations demonstrated here simple, but I do realize that this topic is quite overwhelming. That’s why I encourage you to poke around in the sample project on GitHub that I created for this article and to check out my other article on diffable data sources for a deeper dive. Also, there’s a great session on Apple Developer that introduced the diffable data source that is totally worth checking out.
I hope you have enjoyed reading this article and that the guidance provided here will serve you well. Happy diffing!

