So here we are in WWDC 2019 video 230, and starting at about minute 14 it is claimed that NSFetchedResultsController now vends an NSDiffableDataSourceSnapshot, so we can just apply it directly to a diffable data source (UITableViewDiffableDataSource).
But this is not quite what they say, or what we get. What we get, in the delegate method controller(_:didChangeContentWith:)
, is an NSDiffableDataSourceReference. How do we get from this to an actual snapshot, and what should my diffable data source generic types be?
NSDiffableDataSourceSnapshot stores your sections and items, which the diffable data source references to understand how many sections and cells to display. Just like you created a type alias for UICollectionViewDiffableDataSource, you can create a Snapshot type alias as well.
A representation of the state of the data in a view at a specific point in time. Diffable data sources use snapshots to provide data for collection views and table views. You use a snapshot to set up the initial state of the data that a view displays, and you use snapshots to reflect changes to the data that the view displays.
Use NSDiffableDataSourceSnapshot. Add sections to the data source. Add supplementary views to the data source. Further, you’ll see how easy it is to animate changes with this new type of data source. I hope you’re excited to get started!
The data in a snapshot is made up of the sections and items you want to display, in the order you that you determine. You configure what to display by adding, deleting, or moving the sections and items.
The WWDC video implies that we should declare the data source with generic types of String
and NSManagedObjectID
. That is not working for me; the only way I can get sensible behaviour with animations and row updates is by using a custom value object as the row identifier for the data source.
The problem with a snapshot using NSManagedObjectID
as the item identifier is that, although the fetched results delegate is notified of changes to the managed object associated with that identifier, the snapshot that it vends may be no different from the previous one that we might have applied to the data source. Mapping this snapshot onto one using a value object as the identifier produces a different hash when underlying data changes and solves the cell update problem.
Consider a data source for a todo list application where there is a table view with a list of tasks. Each cell shows a title and some indication of whether the task is complete. The value object might look like this:
struct TaskItem: Hashable {
var title: String
var isComplete: Bool
}
The data source renders a snapshot of these items:
typealias DataSource = UITableViewDiffableDataSource<String, TaskItem>
lazy var dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = item.title
cell.accessoryType = item.isComplete ? .checkmark : .none
return cell
}
Assuming a fetched results controller, which may be grouped, the delegate is passed a snapshot with types of String
and NSManagedObjectID
. This can be manipulated into a snapshot of String
and TaskItem
(the value object used as row identifier) to apply to the data source:
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
) {
// Cast the snapshot reference to a snapshot
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
// Create a new snapshot with the value object as item identifier
var mySnapshot = NSDiffableDataSourceSnapshot<String, TaskItem>()
// Copy the sections from the fetched results controller's snapshot
mySnapshot.appendSections(snapshot.sectionIdentifiers)
// For each section, map the item identifiers (NSManagedObjectID) from the
// fetched result controller's snapshot to managed objects (Task) and
// then to value objects (TaskItem), before adding to the new snapshot
mySnapshot.sectionIdentifiers.forEach { section in
let itemIdentifiers = snapshot.itemIdentifiers(inSection: section)
.map {context.object(with: $0) as! Task}
.map {TaskItem(title: $0.title, isComplete: $0.isComplete)}
mySnapshot.appendItems(itemIdentifiers, toSection: section)
}
// Apply the snapshot, animating differences unless not in a window
dataSource.apply(mySnapshot, animatingDifferences: view.window != nil)
}
The initial performFetch
in viewDidLoad
updates the table view with no animation. All updates thereafter, including updates that just refresh a cell, work with animation.
The diffable data source should be declared with generic types String and NSManagedObjectID. Now you can cast the reference to a snapshot:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String,NSManagedObjectID>
self.ds.apply(snapshot, animatingDifferences: false)
}
This leaves open the question of how you're going to populate the cell. In the diffable data source (self.ds
in my example), when you populate the cell, return to the fetched results controller and fetch the actual data object.
For example, in my table view I am displaying the name
of a Group in each cell:
lazy var ds : UITableViewDiffableDataSource<String,NSManagedObjectID> = {
UITableViewDiffableDataSource(tableView: self.tableView) {
tv,ip,id in
let cell = tv.dequeueReusableCell(withIdentifier: self.cellID, for: ip)
cell.accessoryType = .disclosureIndicator
let group = self.frc.object(at: ip)
cell.textLabel!.text = group.name
return cell
}
}()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With