Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update NSFetchedResultsController using performBackgroundTask

I have an NSFetchedResultsController and I am trying to update my data on a background context. For example, here I am trying to delete an object:

persistentContainer.performBackgroundTask { context in
  let object = context.object(with: restaurant.objectID)
  context.delete(object)
  try? context.save()
}

There are 2 things I don't understand:

  1. I would have expected this to modify, but not save the parent context. However, the parent context is definitely being saved (as verified by manually opening the SQLite file).
  2. I would have expected the NSFetchedResultsController to update when the background content saves back up to its parent, but this is not happening. Do I need to manually trigger something on the main thread?

Obviously there is something I am not getting. Can anybody explain this?

I know that I have implemented the fetched results controller delegate methods correctly, because if I change my code to directly update the viewContext, everything works as expected.

like image 885
ganzogo Avatar asked Nov 30 '16 15:11

ganzogo


2 Answers

Explanation

NSPersistentContainer's instance methods performBackgroundTask(_:) and newBackgroundContext() are poorly documented.

No matter which method you call, in either case the (returned) temporary NSManagedObjectContext is set up with privateQueueConcurrencyType and is associated with the NSPersistentStoreCoordinator directly and therefore has no parent.

See documentation:

Invoking this method causes the persistent container to create and return a new NSManagedObjectContext with the concurrencyType set to privateQueueConcurrencyType. This new context will be associated with the NSPersistentStoreCoordinator directly and is set to consume NSManagedObjectContextDidSave broadcasts automatically.

... or confirm it yourself:

persistentContainer.performBackgroundTask { (context) in
    print(context.parent) // nil
    print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)
}

let context = persistentContainer.newBackgroundContext()
print(context.parent) // nil
print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)

Due to the lack of a parent, changes won't get committed to a parent context like e.g. the viewContext and with the viewContext untouched, a connected NSFetchedResultsController won’t recognize any changes and therefore doesn’t update or call its delegate's methods. Instead changes will be pushed directly to the persistent store coordinator and after that saved to the persistent store.

I hope, that I was able to help you and if you need further assistance, I can add, how to get the desired behavior, as described by you, to my answer. (Solution added below)

Solution

You achieve the behavior, as described by you, by using two NSManagedObjectContexts with a parent-child relationship:

// Create new context for asynchronous execution with privateQueueConcurrencyType  
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Add your viewContext as parent, therefore changes are pushed to the viewContext, instead of the persistent store coordinator
let viewContext = persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.perform {
    // Do your work...
    let object = backgroundContext.object(with: restaurant.objectID)
    backgroundContext.delete(object)
    // Propagate changes to the viewContext -> fetched results controller will be notified as a consequence
    try? backgroundContext.save()
    viewContext.performAndWait {
        // Save viewContext on the main queue in order to store changes persistently
        try? viewContext.save()
    }
}

However, you can also stick with performBackgroundTask(_:) or use newBackgroundContext(). But as said before, in this case changes are saved to the persistent store directly and the viewContext isn't updated by default. In order to propagate changes down to the viewContext, which causes NSFetchedResultsController to be notified, you have to set viewContext.automaticallyMergesChangesFromParent to true:

// Set automaticallyMergesChangesFromParent to true
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
persistentContainer.performBackgroundTask { context in
    // Do your work...
    let object = context.object(with: restaurant.objectID)
    context.delete(object)
    // Save changes to persistent store, update viewContext and notify fetched results controller
    try? context.save()
}

Please note that extensive changes such as adding 10.000 objects at once will likely drive your NSFetchedResultsController mad and therefore block the main queue.

like image 192
Tobi Avatar answered Oct 09 '22 08:10

Tobi


The view context will not update unless you have set it to automatically merge changes from the parent. The viewContext is already set as child of any backgroundContext that you receive from the NSPersistentContainer.

Try adding just this one line:

persistentContainer.viewContext.automaticallyMergesChangesFromParent = true

Now, the viewContext WILL update after the backgroundContext has been saved and this WILL trigger the NSFetchedResultsController to update.

like image 8
Andrew Paul Simmons Avatar answered Oct 09 '22 09:10

Andrew Paul Simmons