UITableViewDiffableDataSource cell provider fails to fetch newly added managed object, with temporary id, when reusing cells

381 views Asked by At

The sample app has a table view that is powered by UITableViewDiffableDataSource that gets data from NSFetchedResultsController. You can add letters of the alphabet to the table view by pressing the plus button. To implement data source, I used this article. The issue is that when I add new item to Core Data NSFetchedResultsController feeds temporary IDs to the cell provider. And when I scroll down and cell provider has to reuse cells, it fails to fetch managed object with temporary ID. It does not, however, happen when the item is added to the area of the table view that is on the screen.

lazy var fetchedResultsController: NSFetchedResultsController<Item> = {
    let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
        
    let sort = NSSortDescriptor(key: #keyPath(Item.name), ascending: true)
    fetchRequest.sortDescriptors = [sort]
        
    let controller = NSFetchedResultsController(
        fetchRequest: fetchRequest,
        managedObjectContext: moc,
        sectionNameKeyPath: nil,
        cacheName: nil
    )
        
    controller.delegate = self
        
    return controller
}()
func configureDiffableDataSource() {
    let diffableDataSource = UITableViewDiffableDataSource<Int, NSManagedObjectID>(tableView: tableView) { (tableView, indexPath, objectID) -> UITableViewCell? in  
        guard let object = try? self.moc.existingObject(with: objectID) as? Item else {
            // Crash happens here.
            fatalError("Managed object should be available.")
        }
            
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell_id", for: indexPath)
        cell.textLabel?.text = object.name

        return cell
    }
    self.diffableDataSource = diffableDataSource
    tableView.dataSource = diffableDataSource
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {

    guard let dataSource = tableView?.dataSource as? UITableViewDiffableDataSource<Int, NSManagedObjectID> else {
        assertionFailure("The data source has not implemented snapshot support while it should.")
        return
        }
    var snapshot = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
    let currentSnapshot = dataSource.snapshot() as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
        
    let reloadIdentifiers: [NSManagedObjectID] = snapshot.itemIdentifiers.compactMap { itemIdentifier in
        guard let currentIndex = currentSnapshot.indexOfItem(itemIdentifier), let index = snapshot.indexOfItem(itemIdentifier), index == currentIndex else {
            return nil
        }
        guard let existingObject = try? controller.managedObjectContext.existingObject(with: itemIdentifier), existingObject.isUpdated else { return nil }
        return itemIdentifier
    }
    snapshot.reloadItems(reloadIdentifiers)
        
    let shouldAnimate = tableView?.numberOfSections != 0
    dataSource.apply(snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>, animatingDifferences: shouldAnimate)
}

Adding try! fetchedResultsController.performFetch() right after saving to Core Data fixes the issue, however, it’s a brute-force solution, which causes double call to controller(_:didChangeContentWith:) delegate method and, sometimes, double animation. Fetching should happen automatically in this case. I wonder, why cell provider fails to fetch data and how to fix this in an efficient way.

@objc func handleAdd() {
    // Add item to Core Data.
    let context = moc
    let entity = Item.entity()
    let item = Item(entity: entity, insertInto: context)
    item.name = "\(letters[counter])" // Adds letters of the alphabet.
    counter += 1
    try! context.save()
    // Manually fetching right after saving doesn’t seem efficient.
    try! fetchedResultsController.performFetch()
}
0

There are 0 answers