NSFetchedResultsController is not showing all results after merging an `NSManagedObjectContextDidSaveNotification`
concurrency, core-data, ios, nsfetchedresultscontroller
Solution
The reason why this is happening is due to how `mergeChangesFromContextDidSaveNotification:` handles updated objects. `NSManagedObjectContext` keeps a record of objects which are in use in the context, these are referred to as registered objects (`NSManagedObjectContext` has methods for accessing and conditionally fetching registered objects). `mergeChangesFromContextDidSaveNotification:` only processes updates for objects which are registered in the context. This has a knock on effect for `NSFetchedResultsControllers` that explains the cause of the problem.
Here's how it plays out:
A FRC is setup with a predicate that doesn't match all objects (thus preventing the objects which do not match the predicate from being registered in the FRCs context).
A second context makes a change to an object which means that it now matches the FRCs predicate. The second context is saved.
The FRCs context processes the `NSManagedObjectContextDidSaveNotification` but only updates its registered objects, therefore it does not update the object which now matches the FRC predicate.
The FRC does not perform another fetch when there's a save, therefore isn't aware that the updated object should be included.
The fix
The solution is to fetch all updated objects when merging the notification. Here's an example merge method:
-(void)mergeChanges:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue, ^{
NSManagedObjectContext *savedContext = [notification object];
NSManagedObjectContext *mainContext = self.managedObjectContext;
BOOL isSelfSave = (savedContext == mainContext);
BOOL isSamePersistentStore = (savedContext.persistentStoreCoordinator == mainContext.persistentStoreCoordinator);
if (isSelfSave || !isSamePersistentStore) {
return;
}
[mainContext mergeChangesFromContextDidSaveNotification:notification];
//BUG FIX: When the notification is merged it only updates objects which are already registered in the context.
//If the predicate for a NSFetchedResultsController matches an updated object but the object is not registered
//in the FRC's context then the FRC will fail to include the updated object. The fix is to force all updated
//objects to be refreshed in the context thus making them available to the FRC.
//Note that we have to be very careful about which methods we call on the managed objects in the notifications userInfo.
for (NSManagedObject *unsafeManagedObject in notification.userInfo[NSUpdatedObjectsKey]) {
//Force the refresh of updated objects which may not have been registered in this context.
NSManagedObject *manangedObject = [mainContext existingObjectWithID:unsafeManagedObject.objectID error:NULL];
if (manangedObject != nil) {
[mainContext refreshObject:manangedObject mergeChanges:YES];
}
}
});
}
Problem
I have an NSFetchedResultsController which fetches objects with a predicate: `isTrash == NO` Most of the time this works as expected, but when an object gets untrashed the fetched results controller does not fetch the untrashed object. What's going wrong?