NSFetchedResultsController + UICollectionViewDiffableDataSource + CoreData - How to diff on the entire object?

I'm trying to use some of the new diffing classes built into iOS 13 along with Core Data. The problem I am running into is that controllerdidChangeContentWith doesn't work as expected. It passes me a snapshot reference, which is a reference to a

NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>

meaning I get a list of sections/Object ID's that have changed.

This part works wonderfully. But the problem comes when you get to the diffing in the collection view. In the WWDC video they happily call

dataSource.apply(snapshot, animatingDifferences: true)

and everything works magically, but that is not the case in the actual API.

In my initial attempt, I tried this:

resolvedSnapshot.appendItems(snapshot.itemIdentifiersInSection(withIdentifier: section).map {
     controller.managedObjectContext.object(with: $0 as! NSManagedObjectID) as! Activity
}, toSection: .all)

And this works for populating the cells, but if data is changed on a cell (IE. the cell title) the specific cell is never reloaded. I took a look at the snapshot and it appears the issue is simply that I have references to these activity objects, so they are both getting updated simultaneously (Meaning the activity in the old snapshot is equivalent to the one in the new snapshot, so the hashes are equal.)

My current solution is using a struct that contains all my Activity class variables, but that disconnects it from CoreData. So my data source became:

var dataSource: UICollectionViewDiffableDataSource<Section, ActivityStruct>

That way the snapshot actually gets two different values, because it has two different objects to compare. This works, but it seems far from elegant, is this how we were meant to use this? Or is it just in a broken state right now? The WWDC video seems to imply it shouldn't require all this extra boilerplate.


Solution 1:

I ran into the same issue and I think I figured out what works:

There are two classes: UICollectionViewDiffableDataSource and UICollectionViewDiffableDataSourceReference

From what I can tell, when you use the first, you're taking ownership as the "Source of Truth" so you create an object that acts as the data source. When you use the second (the data source reference), you defer the "Source of Truth" to another data source (in this case, CoreData).

You would instantiate a ...DataSourceReference essentially the same way as a ...DataSource:

dataSourceReference = UICollectionViewDiffableDataSourceReference(collectionView: collectionView, cellProvider: { (collectionView, indexPath, object) -> UICollectionViewCell? in
    let identifier = <#cell identifier#>
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)

    <#cell configuration#>
            
    return cell
})

And then later when you implement the NSFetchedResultsControllerDelegate, you can use the following method:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference)
{
    dataSourceReference.applySnapshot(snapshot, animatingDifferences: true)
}

I watched the WWDC video as well and didn't see this referenced. Had to make a few mistakes to get here. I hope it works for you!