NSFetchedResultsController with UITableViewDiffableDataSource update value

I have a NSFetchedResultsController combined with a UITableViewDiffableDataSource.

There is a bool attribute "marked" in my entity which I want to change by clicking on the table view cell. To display the status, I change the background color of the cell. However, the reload is not working as expected. The background color of the cell won't change.

I first tried UITableViewDiffableDataSource<String, NSManagedObjectID>, but obviously that won't work for changes, because the NSManagedObjectID doesn't change. I then updated it to UITableViewDiffableDataSource<Int, Test>, but that also didn't work as expected.


Any ideas how to solve this? Or is it just not (yet) possible?



import UIKit
import CoreData

class MainViewController: UITableViewController {
    
    lazy var fetchedResultController: NSFetchedResultsController<Test> =  {
        let fetchRequest: NSFetchRequest<Test> = Test.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
        let fetchedResultController = NSFetchedResultsController<Test>(fetchRequest: fetchRequest, managedObjectContext: self.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultController.delegate = self
        return fetchedResultController
    }()
    
    var persistentContainer: NSPersistentContainer { get { return (UIApplication.shared.delegate as! AppDelegate).persistentContainer } }
    var viewContext: NSManagedObjectContext { get { return self.persistentContainer.viewContext } }

    lazy var dataSource: UITableViewDiffableDataSource<Int, Test> = {
        return UITableViewDiffableDataSource<Int, Test>(tableView: self.tableView) { (tableView, indexPath, cdObject) -> UITableViewCell? in
        
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            
            cell.textLabel?.text = "\(cdObject.name!) (\(cdObject.id))"
            cell.detailTextLabel?.text = DateFormatter.localizedString(from: cdObject.timestamp!, dateStyle: .short, timeStyle: .short)
            cell.backgroundColor = cdObject.clicked ? UIColor.systemOrange : UIColor.systemBackground
            
            return cell
        }
    }()
    
    
    @IBAction func barButtonPressed_insert(_ sender: UIBarButtonItem) {
        for i in 0 ..< 7 {
            let newCDEntry = Test(context: self.viewContext)
            newCDEntry.id = Int16(i)
            newCDEntry.name = "Name \(i)"
            newCDEntry.timestamp = Date()
        }
        
        self.viewContext.transactionAuthor = "regular insert"
        defer { self.viewContext.transactionAuthor  = nil }
        do {
            try self.viewContext.save()
        } catch {
            let nserror = error as NSError
            if let conflicts = nserror.userInfo["conflictList"] as? [NSConstraintConflict] {
                for conflict in conflicts {
                    for conflictObject in conflict.conflictingObjects {
                        self.viewContext.delete(conflictObject)
                    }
                }
            }
            do {
                try self.viewContext.save()
            } catch {
                print("##### error saving insert")
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        do {
            try self.fetchedResultController.performFetch()
        } catch {
            print("could not fetch data")
        }
        self.applyFetchedDataToDataSource(animated: false)
    }
    
    private func applyFetchedDataToDataSource(animated: Bool) {
        var snapshot = NSDiffableDataSourceSnapshot<Int, Test>()
        snapshot.appendSections([0])
        snapshot.appendItems(self.fetchedResultController.fetchedObjects ?? [])
        self.dataSource.apply(snapshot, animatingDifferences: animated)
    }
    
    @objc private func managedObjectContextObjectDidSave(notification: Notification) {
        print("received save notification")
        self.viewContext.mergeChanges(fromContextDidSave: notification)
    }
    
    // MARK: - TableView
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        let cdObject = self.fetchedResultController.object(at: indexPath)
        cdObject.clicked = !cdObject.clicked
        do {
            self.viewContext.transactionAuthor = "click toggled"
            defer { self.viewContext.transactionAuthor  = nil }
            try self.viewContext.save()
        } catch {
            print("could not save clicked toggle")
        }
    }
}

extension MainViewController: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        self.applyFetchedDataToDataSource(animated: true)
    }
}

Replies

The snapshot is only for sections and rows not for changes to the data being displayed in those rows. In your cell handler do not configure the cell instead just set the object on it and within a cell subclass observe NSManagedObjectContextObjectsDidChangeNotification notification and update the cell when necessary. Or you could just update all visible cells in the fetched results controller delegate method.


Also I noticed another issue. Instead of controllerDidChangeContent you need to use the new delegate method didChangeContentWithSnapshot. That is called during the performFetch with the initial snapshot so there is no need to call applyFetchedDataToDataSource.