redPanda
redPanda

Reputation: 797

NSFetchedResultsController not recognizing deletes

I have a multiple number of Core Data entities, all synced to the server. For testing I have built a function that will: 1. Delete all of the core data entities 2. Call APIs to refresh all data

     // Next delete data in all entities (but not user)
    for entity in CoreDataStack.sharedInstance.allEntities() {
        if (entity != "User") {
            if (DataAccess.deleteExisting(entityName: entity, inMoc: self.operationMOC)) {
                print("Deleted OK: \(entity)")
            } else {
                print("ERROR deleting \(entity)")
            }
        }
    }

    ....

    // Save updates
    self.operationMOC.performAndWait {
        do {
            try self.operationMOC.save()
        } catch {
            fatalError("ERROR: Failed to save operation moc: \(error)")
        }
    }
    self.mainThreadMOC.performAndWait {
        do {
            try self.mainThreadMOC.save()
        } catch {
            fatalError("ERROR: Failed to save main moc: \(error)")
        }
    }

The problem is that the NSFetchedResultsController controlling my UI seems not to recognize the delete, giving an error when the "didChange" delegate method fires for the update

error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (14) must be equal to the number of rows contained in that section before the update (7), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)

I have split the refresh function into 2 parts so I can see that the delete is never recognized, hence there are too many UITableView rows when the update fires and tries to tableView.insertRows(at: ...). I have also tried direct updates to the MOC on my main thread - no luck

I have also put in an observer:

NotificationCenter.default.addObserver(self, selector: #selector(MessagesTab.dataChanged), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: self.mainThreadMOC)

Which fires perfectly on delete and update, so Core Data is doing it's job.

Consequently, my question before I dump the NSFetchResults Controller and roll my own using notifications, is does anyone have any idea what I am doing wrong here? (either in code or expectations) I have been stumped on this for the past day so any advice would be gratefully received.

My NSFetchedResultsControllerDelegate didChange method:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    print(">>>> FRC Change Fired")
    switch type {
    case .insert:
        tableView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
        tableView.deleteRows(at: [indexPath!], with: .automatic)
    case .update:
        let rowMessage = self.fetchedResultsController.object(at: indexPath!)
        if let cell = self.tableView.cellForRow(at: indexPath!) as? InterestsListCell {
            cell.configureWithData(rowMessage)
        }
    default:
        print("Was not a update, delete or insert")
    }
}

Upvotes: 3

Views: 2255

Answers (5)

Deitsch
Deitsch

Reputation: 2198

As you can read in the other answers/comments NSBatchDeleteRequest are performed on a SQL level thus are not recognised by the FetchResultsController (RFC). To reflect deletes on the RFC one has two options:

1. Sync deletes back into the context

Stick with the NSBatchDeleteRequest and sync back the changes like Apple describes in their documentation.
The NSBatchDeleteRequest only works for SQLite persistent stores.
The NSBatchDeleteRequest is not compatible with any validation rule.

// create deleteRequest
let deleteReuqest = NSBatchDeleteRequest(objectIDs: toDelete.map(\.oid))

// define that the result are the deleted IDs 
deleteReuqest.resultType = .resultTypeObjectIDs

// run the BatchDelete
let deleteResult = try context.execute(deleteReuqest) as? NSBatchDeleteResult

// gather the deletedIds and sync the changes into the context
if let objectIDs = deleteResult?.result as? [NSManagedObjectID] {
    NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs], into: [context])
}

2. Use single deletes

If you don't have to delete a lot of objects at once it may make sense to stop using the BatchDelete completely and switch to deleting single objects. Single deletes are not on an SQL level thus immediately recognised by the RFC without any extra work.

// run this for every object you want to delete
context.delete(objectToBeDeleted)

Some help to pick one over the other

If you have a large number of objects to delete -> NSBatchRequest
If you want to rely on validation rules -> context.delete(...)

Upvotes: 0

Tim
Tim

Reputation: 1

As mentioned above, the NSBatchDeleteRequest does not reflect changes in the context. Nil-ing out the FRC could cause problems updating a UITableView when the new FRC is added.

context.refreshAllObjects() is the way to go but to add to @infinateLoop's answer (I'm unable to comment):

func deleteAndUpdate() {

    DeleteRecords(moc: context) // your NSBatchDeleteRequest function here

    context.refreshAllObjects()
    try? frc.performFetch() // catch error if required
    myTableView.reloadData()
}

Upvotes: 0

devjme
devjme

Reputation: 718

I ended up nil-ing out my FRC then rebuilding it after the batch delete completes

    fetchedResultsController = nil

    dataProvider?.deleteAllAlertsBatch  { error in
        DispatchQueue.main.async {
            print(" done delete \(error)")
            self.loadSavedData() // <- build the FRC again here - it will find 0 records

        }
    }

Upvotes: 0

infiniteLoop
infiniteLoop

Reputation: 2179

This is expected behaviour as the batch deletion/updation happens directly on Store as @redPanda pointed out. You can try refreshing the context (generally 'main' as its the context with which the FRC should be attached to) using

context.refreshAllObjects()

Upvotes: 0

redPanda
redPanda

Reputation: 797

Many thanks to @JonRose This code will NOT cause the FRC to fire

    // Delete all records for the entity
    func deleteExisting(entityName: String, inMoc moc: NSManagedObjectContext) -> Bool {
    do {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
        try moc.execute(deleteRequest)
        return true
    } catch {
        print("ERROR: deleting existing records - \(entityName)")
        return false
    }
}

Subsequently I have researched this and hope that it will help others - see WWDC 2015 session on Core Data where this slide was shown:enter image description here I have tried using .refreshAll() and .mergeChages, but the simples for me seemed to be to ditch my FRC and use NSManagedObjectContextDidSave notification

Upvotes: 8

Related Questions