NSFetchedResultsController doesn't show updates from a different context

Lorenzo B picture Lorenzo B · May 3, 2012 · Viewed 10k times · Source

I have an NSFetchedResultsController and a few operations updates managed objects on separate threads via NSOperationQueue.

The FRC (with its predicate) looks like this:

- (NSFetchedResultsController*)fetchedResultsController
{
    if(fetchedResultsController) return fetchedResultsController;

    NSManagedObjectContext* mainContext = [self managedObjectContext];

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    [fetchRequest setEntity:[NSEntityDescription entityForName:@"Check" inManagedObjectContext:mainContext]];
    [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"isSync == %@", [NSNumber numberWithBool:NO]]];
    [fetchRequest setFetchBatchSize:10];

    fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:mainContext sectionNameKeyPath:nil cacheName:nil];
    fetchedResultsController.delegate = self;

    [fetchRequest release], fetchRequest = nil;

    return fetchedResultsController;
}

The main thread and the threaded operation have their own managed object contexts. They only share the same coordinator.

Within the threaded operation I change the isSync property from NO to YES. To know what is Check entity to update, the main context passes to the threaded one a NSManagedObjectID. The threaded operation retrieves the managed object like the following:

-(void)main
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSManagedObjectContext *exportContext = [[NSManagedObjectContext alloc] init];
    [exportContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];

    //...

    Check* check = (Check*)[exportContext existingObjectWithID:objID error:&error];
    check.isSync = [NSNumber numberWithBool:YES];

    //...

    [exportContext save:&error];

    [pool release], pool = nil;
}

When the thread operation calls a save the mergeChangesFromContextDidSaveNotification notification is called and the main context merges the changes.

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == [self managedObjectContext]) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}

Logging the description of the notification leads to verify that changes are performed correctly.

My problem

Delegates methods of NSFetchedResultsControllerDelegate are not called.

This is quite strange since dealing with the same context, the main one, allows to listen for changes and delegates methods are called, e.g. deleting a row object in the UITableView.

I've found some topics on SO with the same problem. I've tried all the workarounds but I cannot find a valuable solution:

  1. NSFetchedResultsController not showing updates from other contexts

  2. NSFetchedResultsController not firing delegate method after merging update from background thread

  3. NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext

Thank you in advance.

Edit

The code above was working in a previous model. Then I created a new model copying (and pasting) entities from the previous one and now it doesn't work anymore.

Suggestions?

Edit 2

This is the predicate I'm using in NSFetchedResultsController getter. It's my fault, but when I wrote the post I didn't copy it.

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"insertionDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

// previous code here
[fetchRequest setSortDescriptors:sortDescriptors];

Now, about Jody last comment

In the main() of your NSOperation, you are loading new objects, and in there it looks like you are setting isSync to YES for each new object. The predicate you use for the fetchedResultsController is looking only for objects that have isSync == NO.

I expecting that when the property isSync is set to YES, the NSFetchedResultsController observes that changes and removes rows that not match the predicate. Am I wrong?

Remember that when merging changes from the background to the main thread, I'm able to see that few objects have updated their isSync property.

Answer

Jody Hagins picture Jody Hagins · May 7, 2012

You have the basic idea, so there is probably a bug somewhere in your code...

Double check that you are properly registering to receive the notification from the background MOC.

Register to receive all notifications from all objects. In that method, log the event, and all its data. When the object is a MOC, dump all its properties (especially the lists of registered, inserted, updated, and deleted objects).

Put a log statement right before and after the save call, and in the notification handler for merging the notification.

Also, you omitted a lot of code so it's hard to know what you are actually doing, but the code sample you included looks like it is hard setting isSync to YES for all objects being loaded, but your fetch request only wants those with isSync set to NO. None of those new objects will pass that predicate.

Finally, double check your model definition and make sure you are using the right number type. This can be a big source of problems.

EDIT

Oh yeah, I forgot... your fetch request does not have a sort descriptor. When you create a FRC, your fetch request must contain at least one sort descriptor... if you have multiple sections, the first sort descriptor is used to group the objects into sections.

To follow up on Alexsander's comment... I alluded to it at the beginning of my post, but you certainly do not want to listen to notifications from a MOC unless it is well known as one of yours (unless, of course, you are just logging for debugging purposes). You should know about the MOC you are using.

Furthermore, I would suggest using parent/child MOCs for this type of processing, but what you are doing should work if done properly.

Parent (private concurrency type) Main (main concurrency type)

Then, with your background MOCs, just have them set the main moc as their parent. When they save, their objects get injected directly into the main MOC. The main MOC can then issues saves at later times to put them onto disk.

Or, you can parent you background MOC to the "parent" and then the "main" MOC can just reissue the fetch to get the data from the parent.