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.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…