Unable to cast NSDiffableDataSourceSnapshotReference

Using the iOS 13 beta SDK, I'm trying to drive updates to a UICollectionView using a NSFetchedResultsController and the new NSDiffableDataSource APIs as outlined in session 230 - 'Building Apps with Core Data'.


Using the NSFetchedResultsController delegate method 'didChangeContent withSnapshot', I'm trying to for cast the NSDiffableDataSourceSnapshotReference to a NSDiffableDataSourceSnapshot as shown in the slides. However, I'm running into a compile error when trying to do so - "Generic parameter 'SectionIdentifierType' could not be inferred in cast to 'NSDiffableDataSourceSnapshot'".


func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference<NSManagedObjectID, NSString>) {
        
        dataSource.apply(snapshot as! NSDiffableDataSourceSnapshot, animatingDifferences: true)
    }


Is Anyone else seeing this error? Any thoughts on how I can get past it?


I've also filed a feedback report - FB6135082.


Thanks,


Andrew

Replies

This doesn't look ready, yet. The example is clearly wrong, as the cast operates on unrelated types and always fails when you add the missing generic paramaters, given the following class declarations in beta 1:


open class NSDiffableDataSourceSnapshotReference<sectionidentifiertype, itemidentifiertype=""> : NSObject, NSCopying where SectionIdentifierType : AnyObject, ItemIdentifierType : AnyObject {
public class NSDiffableDataSourceSnapshot<sectionidentifiertype, itemidentifiertype=""> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {

Also, if you try to use the NSDiffableDataSourceSnapshotReference that is passed to your delegate in controller(_, didChangeContentWith:), you get all sorts of type errors which seem to indicate a problem with how this type is imported into Swift:


(lldb) po snapshot.sectionIdentifiers
Precondition failed: NSArray element failed to match the Swift Array Element type
Expected NSManagedObjectID but found __NSCFConstantString: 
…

(lldb) po snapshot.itemIdentifiers
Precondition failed: NSArray element failed to match the Swift Array Element type
Expected NSString but found NSTemporaryObjectID_0: 
…


This lets me think that NSDiffableDataSourceSnapshotReference<NSManagedObjectID, NSString> should probably be NSDiffableDataSourceSnapshotReference<NSString, NSManagedObjectID>.

Can confirm this issue when I try to use this on a UITableView. I hope Apple addresses this in a future beta. Everyone send a bugreport!

Not yet fixed in beta 2.

Maybe this question is related: What is the difference between NSDiffableDataSourceSnapshot and NSDiffableDataSourceSnapshotReference?

From what I can see its the Reference version just enforces reference types, i.e. classes.

It looks as if this isn't supported directly yet. However I did manage to get it to work by pulling out the associated data. This assumes a single section so you'll need to adjust for your own implenentation however it does work and appears to update, animate etc as expected. Given the Apple sample code I was also able to add the publisher approach for cell field updates and also remove the dataSource methods

for numberOfItems, etc...


<nsmanagedobjectid, nsstring=""><section, tag="">

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference<NSManagedObjectID, NSString>) {
         // NOTE: Section.main is an enum that represents my single section
        let diffable = NSDiffableDataSourceSnapshot<Section, Tag>()
        diffable.appendSections([.main])
        diffable.appendItems(dataProvider.fetchedResultsController.fetchedObjects ?? [], toSection: .main)
        dataSource.apply(diffable, animatingDifferences: true)
    }

I'm also running into this issue, but a bit of type finagling I have it mostly working. My remaining issue is that I can't figure out how to properly populate the collection view on load.


My type order is, as IngmarStein suggests, string before object ID,


    typealias Snapshot = NSDiffableDataSourceSnapshotReference<NSString, NSManagedObjectID>
    typealias DataSource = UICollectionViewDiffableDataSourceReference<NSString, NSManagedObjectID>


extension ViewController: NSFetchedResultsControllerDelegate {
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference<NSManagedObjectID, NSString>) {
        let newSnapshot = Snapshot()
        newSnapshot.appendSections(withIdentifiers: snapshot.sectionIdentifiers as [AnyObject] as! [NSString])
        newSnapshot.appendItems(withIdentifiers: snapshot.itemIdentifiers as [AnyObject] as! [NSManagedObjectID])
        self.dataSource.applySnapshot(newSnapshot, animatingDifferences: true)
    }
}

The secionIdentifiers are constant each launch, but I can't see how to get them out of NSFetchedResultsController without calling didChangeContent. Notably, section.name is nil. I can populated on view load by inventing temporary section titles


        let snapshot = Snapshot()
        if let sections = self.fetched.sections {
            let sectionMap = [NSString: NSFetchedResultsSectionInfo](uniqueKeysWithValues: sections.map {section -> (NSString, NSFetchedResultsSectionInfo) in return (UUID().description as NSString, section)})
            snapshot.appendSections(withIdentifiers: sectionMap.compactMap {$0.key})
            for section in sectionMap {
                snapshot.appendItems(withIdentifiers: (section.value.objects as! [NSManagedObject]).map {$0.objectID}, intoSectionWithIdentifier: section.key)
            }
        }
        self.dataSource.applySnapshot(snapshot, animatingDifferences: false)

These IDs will be replaced on first change. I don't know what effect this might have on a more complicated fetch, but so far it seems to not animate a large rearrangement of existing objects into a new section.


I have also submitted a feedback request, FB6162037.

Tried in Beta 3 but no luck. Has anyone else tried in Beta 3?

I was able to get the NSDiffableDataSourceSnapshotReference working in beta 3. Here is my controller::didChangeContentWith method:


    func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference<nsstring, nsmanagedobjectid="">) {
        let resolvedSnapshot = NSDiffableDataSourceSnapshot<string, model="">()

        for section in snapshot.sectionIdentifiers {
            resolvedSnapshot.appendSections([section as String])
            resolvedSnapshot.appendItems(snapshot.itemIdentifiersInSection(withIdentifier: section).map { 
               controller.managedObjectContext.object(with: $0) as! Model 
            }, toSection: section as String)
        }

        dataSource.apply(resolvedSnapshot, animatingDifferences: true)
    }


dataSource is defined as:

dataSource = UICollectionViewDiffableDataSource<String,Model>(collectionView: collectionView) { ... }


I'm curious as to whether this mapping from one snapshot to the snapshot that the dataSource expects is going to be the typical approach.

Did you receive any updates from your feedback request?

It's a straightforward cast:


func controller(_ controller: NSFetchedResultsController, 
  didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
    let snapshot = snapshot as NSDiffableDataSourceSnapshot<string,nsmanagedobjectid>
    self.ds.apply(snapshot, animatingDifferences: false)
}


I'm assuming here that your diffable data source is typed with generic resolutions String, NSManagedObjectID. In your cell population function you go back to the fetched results controller, fetch out the actual object by index path, and populate the cell from its properties.