7

I'm having an issue getting accurate updates from an NSFetchedResultsControllerDelegate after using an NSBatchDeleteRequest, the changes come through as an update type and not the delete type on the didChange delegate method. Is there a way to get the changes to come in as a delete? (I have different scenarios for delete vs update).

The delete request: (uses a private context supplied by the caller)

fileprivate class func deletePeopleWith(ids: Set<Int>, usingContext context: NSManagedObjectContext) {

    let fr: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Person")

    var predicates: [NSPredicate] = []
    ids.forEach {

        predicates.append(NSPredicate(format: "id = %ld", $0))
    }

    fr.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)

    let dr = NSBatchDeleteRequest(fetchRequest: fr)
    dr.affectedStores = context.parent?.persistentStoreCoordinator?.persistentStores

    do {

        try context.parent?.execute(dr)
        context.parent?.refreshAllObjects()
        print("Deleting person")

    } catch let error as NSError {

        let desc = "Could not delete person with batch delete request, with error: \(error.localizedDescription)"
        debugPrint(desc)
    }

}

Results in:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

    DispatchQueue.main.async {

        let changeDesc = "Have change with indexPath of: \(indexPath), new index path of: \(newIndexPath) for object: \(anObject)"
        var changeType = ""
        switch type {

        case .delete:
            changeType = "delete"

        case .insert:
            changeType = "insert"

        case .move:
            changeType = "move"

        case .update:
            changeType = "update"

        }

        print(changeDesc)
        print(changeType)
    }

}

Printing:

Have change with indexPath of: Optional([0, 0]), new index path of: Optional([0, 0]) for object: *my core data object*
update
Fred Faust
  • 6,696
  • 4
  • 32
  • 55
  • You are executing the delete on the parent context and you said that you want to delete using the supplied private context. – ELKA Nov 13 '16 at 16:52
  • @ELKA the fetched results controller is created using the parent context, the parent context is also the context who's parent is the persistent store where the records should be deleted. – Fred Faust Nov 13 '16 at 17:19
  • So context.parent would be a main NSManagedObjectContext which is the same as the NSFetchedResultController's context. Correct? – ELKA Nov 13 '16 at 17:24
  • @ELKA yup, that's it. – Fred Faust Nov 13 '16 at 17:25
  • Can you try `context.parent?.processPendingChanges()` instead of `refreshAllObjects()` – ELKA Nov 13 '16 at 17:29
  • @ELKA I tried, it doesn't call the delegate method on the results controller. – Fred Faust Nov 13 '16 at 23:29
  • Can you update your answer and explain more how are you calling the batch delete and how are you setting up the resultController? – ELKA Nov 14 '16 at 05:25
  • Make sure that the results controller fetch comes after the batch delete. Otherwise you won't be notified. – ELKA Nov 14 '16 at 05:27

3 Answers3

13

From Apple's documentation:

Batch deletes run faster than deleting the Core Data entities yourself in code because they operate in the persistent store itself, at the SQL level. As part of this difference, the changes enacted on the persistent store are not reflected in the objects that are currently in memory.

After a batch delete has been executed, remove any objects in memory that have been deleted from the persistent store.

See the Updating Your Application After Execution section for handling the objects deleted by NSBatchDeleteRequest.

Community
  • 1
  • 1
bteapot
  • 1,897
  • 16
  • 24
  • 3
    The docs alone didn't work but updating the result type to object ID on the delete request and then calling refresh all objects on the context used by the fetched results controller got the change to come in as delete! – Fred Faust Nov 14 '16 at 12:35
  • 3
    The documentation doesn't explicitly show the code for this, but it does state: "To do this, first make sure the `resultType` of the `NSBatchDeleteRequest` is set to `NSBatchDeleteRequestResultType.resultTypeObjectIDs` before the request is executed." So be sure to include: `deleteRequest.resultType = .resultTypeObjectIDs`. – blwinters Apr 11 '18 at 14:15
  • Thanks so much, that was exactly what I was looking for! – Johannes Fahrenkrug Feb 20 '20 at 18:35
2

Here is the sample, works for me. But i didn't find how to update deletion with animation, cause NSFetchedResultsControllerDelegate didn't fire.

@IBAction func didPressDelete(_ sender: UIBarButtonItem) {
        let managedObjectContext = persistentContainer.viewContext
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: Record.self))
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)

        do {
            // delete on persistance store level
            try managedObjectContext.execute(deleteRequest)

            // reload the whole context
            managedObjectContext.reset()
            try self.fetchedResultsController.performFetch()
            tableV.reloadData()
        } catch {
            print("")
        }
    }

Thanks.

Nike Kov
  • 12,630
  • 8
  • 75
  • 122
1

Since NSBatchDeleteRequest deletes the managed objects at the persistentStore level (on disk), the most effective way to update the current context (in-memory) is by calling refreshAllObjects() on the context like so:

func batchDelete() {
    if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
        let context = appDelegate.persistentContainer.viewContext
        if let request = self.fetchedResultsController.fetchRequest as? NSFetchRequest<NSFetchRequestResult> {
            let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: request)
            do {
                try context.execute(batchDeleteRequest) // performs the deletion
                appDelegate.saveContext()
                context.refreshAllObjects() // updates the fetchedResultsController
            } catch {
                print("Batch deletion failed.")
            }
        }
    }
}

No need for refetching all the objects or tableView.reloadData()!

Finn
  • 11
  • 4
  • I think the docs linked in the accepted answer should be followed, my original comment/ question has refresh all objects but the way to do it these days would be to merge the changes into the target context(s) from the result of the delete request. – Fred Faust Sep 28 '20 at 02:59