Why an invisible same cell is being updated when update is performing using NSFetchedResultsController and Diffable Data Source?

Introduction

I expect when I perform "update" on 1 of the CoreData entity content, corresponding UICollectionViewCell should updated seamlessly.

But, the thing doesn't work as expected.

After debugging, I notice that instead of updating visible UICollectionViewCell on screen, an invisible offscreen UICollectionViewCell has been created and all update operations are performed on the invisible UICollectionViewCell?!


Simple hookup between NSFetchedResultsController and Diffable Data Source

The hookup is pretty straightforward

extension ViewController: NSFetchedResultsControllerDelegate {
    func controller(_ fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshotReference: NSDiffableDataSourceSnapshotReference) {
            
        guard let dataSource = self.dataSource else {
            return
        }
        
        let snapshot = snapshotReference as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
        
        print("dataSource.apply")
        
        dataSource.apply(snapshot, animatingDifferences: true) {
        }
    }
}

Simple data structure

import Foundation
import CoreData


extension NSTabInfo {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<NSTabInfo> {
        return NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
    }

    @NSManaged public var name: String?
    @NSManaged public var order: Int64

}

extension NSTabInfo : Identifiable {

}

Simple data source to update cell

private func initDataSource() {
    let dataSource = DataSource(
        collectionView: collectionView,
        cellProvider: { [weak self] (collectionView, indexPath, objectID) -> UICollectionViewCell? in
            
            guard let self = self else { return nil }
            
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: "cell",
                for: indexPath) as? CollectionViewCell else {
                return nil
            }

            guard let nsTabInfo = self.getNSTabInfo(indexPath) else { return nil }
            cell.label.text = nsTabInfo.name
            
            print("Memory for cell \(Unmanaged.passUnretained(cell).toOpaque())")
            print("Content for cell \(cell.label.text)\n")
            
            return cell
        }
    )
    
    self.dataSource = dataSource
}

Updating code doesn't work

We perform update on the 1st cell using the following code

@IBAction func updateClicked(_ sender: Any) {
    let backgroundContext = self.backgroundContext
    
    backgroundContext.perform {
        let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
        
        fetchRequest.sortDescriptors = [
            NSSortDescriptor(key: "order", ascending: true)
        ]
        
        do {
            let nsTabInfos = try fetchRequest.execute()
            
            if !nsTabInfos.isEmpty {
                // Perform update on the first cell
                
                nsTabInfos[0].name = "\(Int(nsTabInfos[0].name!)! + 1)"
                
                if backgroundContext.hasChanges {
                    try backgroundContext.save()
                }
            }
        } catch {
            print("\(error)")
        }
    }
}

Nothing is changed on the screen. However, if we look at the print output. We can see the initial content ("0") of the 1st cell is updated to "1". But, all of these are being done in an invisible same instance cell.

dataSource.apply
Memory for cell 0x0000000138705cd0
Content for cell Optional("1")

Memory for cell 0x0000000138705cd0
Content for cell Optional("1")

Memory for cell 0x0000000138705cd0
Content for cell Optional("2")

Memory for cell 0x0000000138705cd0
Content for cell Optional("3")

Ordering work without issue

Changing the ordering of the cell works as expected. The following is the code for charging ordering.

@IBAction func moveClicked(_ sender: Any) {
    let backgroundContext = self.backgroundContext
    
    backgroundContext.perform {
        let fetchRequest = NSFetchRequest<NSTabInfo>(entityName: "NSTabInfo")
        
        fetchRequest.sortDescriptors = [
            NSSortDescriptor(key: "order", ascending: true)
        ]
        
        do {
            let nsTabInfos = try fetchRequest.execute()
            
            for (index, element) in nsTabInfos.reversed().enumerated() {
                element.order = Int64(index)
            }
            
            if backgroundContext.hasChanges {
                try backgroundContext.save()
            }
        } catch {
            print("\(error)")
        }
    }
}

Do you have idea why such problem occur? I prefer not to have collectionView.reloadData as workaround, as it will create more issue (like resetting scroll position, cell press state, ...)

I posted a complete demo at https://github.com/yccheok/problem-update-frc-diffable

Thank you

sigh…

for reasons that boggle my mind, the snapshot returned by the FRC

        let snapshot = snapshotReference as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>

DOES NOT properly handle object updates, only inserts, deletions, and reorder. Its infuriating, frankly.

So I have resorted to implementing my FRC delegate like this…

    var changedCars: [Car] = []

    func updateSnapshot() {
        var diffableDataSourceSnapshot = NSDiffableDataSourceSnapshot<String, Car>()
        frc.sections?.forEach { section in
            diffableDataSourceSnapshot.appendSections([section.name])
            diffableDataSourceSnapshot.appendItems(section.objects as! [Car], toSection: section.name)
        }
        diffableDataSourceSnapshot.reloadItems(changedCars)
        
        diffableDataSource?.apply(diffableDataSourceSnapshot, animatingDifferences: true)
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        
        if type == .update, let car = anObject as? Car {
            changedCars.append(car)
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        self.updateSnapshot()
        
        changedCars.removeAll()
    }
Why an invisible same cell is being updated when update is performing using NSFetchedResultsController and Diffable Data Source?
 
 
Q