Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a diffable snapshot from an NSFetchResultsController in iOS 13?

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?

like image 861
matt Avatar asked Oct 20 '19 17:10

matt


People also ask

What is nsdiffabledatasourcesnapshot in Salesforce?

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.

What is a snapshot in Salesforce?

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.

How to animate changes with nsdiffabledatasourcesnapshot?

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!

What is the data in a snapshot?

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.


2 Answers

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.

like image 187
san-ho-zay Avatar answered Oct 13 '22 00:10

san-ho-zay


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
    }
}()
like image 11
matt Avatar answered Oct 13 '22 02:10

matt