4

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?

Benedict Cohen
  • 11,912
  • 7
  • 55
  • 67
  • possible duplicate of [NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext](http://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different) - Note that the answer to that question is slightly different from your solution. – Martin R Apr 30 '13 at 09:41
  • @MartinR I agree that the questions are very similar. But is it possible to merge them so that my answer still appears? (I think my answer is superior to the accepted answer on the other question as mine better explains the problem and provides a better solution too.) – Benedict Cohen Apr 30 '13 at 11:12
  • I don't think that any answer disappears only because a question is marked as a duplicate. But I have no experience if/when/how questions are merged at all. - Your explanation is good (similar to mine http://stackoverflow.com/a/14021815/1187415 :-). I don't know which solution is better, so if you have arguments in favor of your method, I would be interested to know them! – Martin R Apr 30 '13 at 11:23
  • @MartinR the reason why I think my solution is superior is the use of `existingObjectWithID:error:`/`refreshObject:mergeChanges:` instead of `objectWithID:`/`willAccessValueForKey:`. `existingObjectWithID:` will side step any issues due to invalid `objectId`s (although this is unlikely to happen) and `refreshObject:mergeChanges:` is a more explicit way of forcing an object to be fetched (`willAccessValueForKey:` is an mis-use of KVO. `willAccessValueForKey:` should only be used internally and should have a matching `didAccessValueForKey:`). – Benedict Cohen Apr 30 '13 at 12:12
  • But it is [documented](https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObject_Class/Reference/NSManagedObject.html#//apple_ref/occ/instm/NSManagedObject/willAccessValueForKey:) that you can use `willAccessValueForKey` to ensure that a fault has been fired. – Martin R Apr 30 '13 at 12:16
  • @MartinR fair enough - I didn't know that (I still think it's a misuse of KVO). I still prefer my approach as I think `refreshObject:mergeChanges:` is more explicit and for code like this I think it's particularly important to clearly express what the code is doing and why. – Benedict Cohen Apr 30 '13 at 13:31
  • See my answer for this Question. I think this is could help: http://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different – Artur Friesen Oct 01 '13 at 16:27

2 Answers2

20

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:

  1. 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).

  2. A second context makes a change to an object which means that it now matches the FRCs predicate. The second context is saved.

  3. 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.

  4. 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];
            }
        }                
    });
}
Benedict Cohen
  • 11,912
  • 7
  • 55
  • 67
  • Or you just set `shouldRefreshRefetchedObjects` to `YES` on the `NSFetchRequest` for the fetched results controller. – Mike Weller Apr 30 '13 at 09:29
  • @lostintranslation `UOV_async_main_thread` was a macro. I've replaced it with a call to the underlying function. – Benedict Cohen Dec 29 '14 at 09:25
  • 1
    @BenedictCohen your explanation has been the starting point for an in-depth evaluation of NSFRC. It brings some nuance to the statements made above and proposes alternative solutions. It can be found here: https://medium.com/bpxl-craft/nsfetchedresultscontroller-woes-3a9b485058#.c9k95uae0 – MiKL Aug 17 '16 at 12:44
-1

Try setting shouldRefreshRefetchedObjects to YES on the NSFetchRequest for your fetched results controller.

This provides more convenient way to ensure managed object property values are consistent with the store than by using refreshObject:mergeChanges: (NSManagedObjetContext) for multiple objects in turn.

Mike Weller
  • 45,401
  • 15
  • 131
  • 151
  • This options probably has only effect if you call `performFetch` on the FRC again. The problem here is that `mergeChangesFromContextDidSaveNotification` does not update objects that are not loaded in the current context or are a fault. Therefore the automatic change tracking of the FRC does not work. See also http://stackoverflow.com/a/14021815/1187415 for a similar analysis of the problem. (I did not downvote though :-) – Martin R Apr 30 '13 at 11:07