Collection views and table views need to know what data to display, where to display it and how to move it. Data sources give our collection views and table views of this data. Diffable data sources provide a more simple and more stateless way to provide our data.
What is it?
A diffable data source is a subclass of either UITableViewDataSource
or UICollectionViewDataSource
. It is generic over two Hashable
types. The types are SectionIdentifierType
and ItemIdentifierType
.
SectionIdentifierType
defines the sections to show in your table view or collection view. Many single section implementations simply use an enum.
enum Section {
case .main
}
ItemIdentifierType
defines the data or models that support individual cells. A basic cell displaying an image and label could be modelled with a Struct
.
struct CellModel {
let title: String
let imageIdentifier: String
}
The generic constraints of the diffable data source tell the data source which sections will exist and what type of models will provide data to cells.
The diffable data source also needs to now what cells to show. The initialiser of UITableViewDiffableDataSource
shows how this is achieved.
public typealias CellProvider = (_ tableView: UITableView,
_ indexPath: IndexPath,
_ itemIdentifier: ItemIdentifierType)
-> UITableViewCell?
public init(tableView: UITableView,
cellProvider: @escaping CellProvider)
The initialiser requires a UITableView
and a CellProvider
, which is a function. The function takes a table view, index path and item identifier and returns an optional UITableViewCell
. The CellProvider
function is what tells the data source what cells to display. Typically the function involves dequeueing reusable cells, configuring them and returning them.
With this in mind creating a UITableViewDiffableDataSource
for a UIViewController
might look like the following.
func makeDataSource(tableView: UITableView) -> UITableViewDiffableDataSource<Section, CellModel> {
UITableViewDiffableDataSource(tableView: tableView, cellProvider: cellProvider)
}
func cellProvider(tableView: UITableView,
indexPath: IndexPath,
model: CellModel) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath) as! Cell
cell.textLabel.text = model.title
cell.imageView.image = UIImage(named: model.imageIdentifier)
return cell
}
The data source cannot be initialised until a table view has been initialisd because the data source requires a table view. Because of this a lazy variable would be an appriopriate property on the view controller.
lazy var dataSource = makeDataSource(tableView: tableView)
Notice the injection of a UITableView
is unlike the older UITableViewDataSource
. Previously data sources were assigned as variables of a table view. The table view would request information about it’s data from the data source. Think cellForRowAt
and numberOfRowsInSection
. This often required the data source to observe, store and often share state with other classes like a parent view controller or a supporting view model.
Diffable data sources are injected with a UITableView
or UICollectionView
to reduce the exposed need for shared state as they can reference the views directly. This makes the diffable data source API much more concise and easier to reason about.
Snapshots
A core concept behind diffable data sources are snapshots. A NSDiffableDataSourceSnapshot
is a snapshot of the state of the data that you want to render. Updating your table view or collection view is simply an act of configuring a Snapshot
and asking the data source to apply
it.
Like the diffable data sources, a Snapshot
is also generic over SectionIdentifierType
and ItemIdentifierType
. These types should match the types of the diffable data source.
The Snapshot
API provides a number of methods to configure its state. The method inputs depend on provided section and identifier types.
// NSDiffableDataSourceSnapshot<Section, CellModel> provides...
func appendSections(_ identifiers: [Section])
func appendItems(_ identifiers: [CellModel], toSection: Section?)
func insertItems(_ identifiers: [CellModel], afterItem: CellModel)
func moveItem(_ identifier: CellModel, afterItem: CellModel)
func deleteItems(_ identifiers: [CellModel])
Once sections and models are added to the snapshot, calling apply
on the diffable data source tells it to update the view.
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, CellModel>
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(models, toSection: section)
dataSource.apply(snapshot, animatingDifferences: true)
After calling apply
the diffable data source leverages the fact its section and item types are Hashable
to compute the difference between its current snapshot and the one it is about to apply. It only applies the necessary changes. If no models changed state or position it would do no work. If two models switched places it would only update the two relevant cells.
Selecting
Selecting elements in a diffable data source driven table view can be done through the didSelectRowAt
method of the table view delegate. Diffable data sources allow us to get items and sections from index paths and ints by using the itemIdentifier(for: IndexPath)
and sectionIdentifier(for: Int)
methods.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
// do something with the item
}
Benefits
- DiffableDataSources give us simple and concise code to back fast and effective table views and collection views with data.
- The
apply(snapshot)
API gives us a clear entry point into updating the view state, without having to retain state in multiple places. - Supporting view models can think strictly in output, where the output of any view model is the input to
apply(snapshot)
.
Remember
- Be
Hashable
andEquatable
. If you do any custom hashing or equating, make sure you do it right so that model states are always different. If you don’t you’ll get unexpected display updates.
Links
- Supporting GitHub example: https://github.com/mgopsill/MGDiffableDataSource