NSFetchedResultsController doesn't show updates from a different context

10,309

Solution 1

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.

Solution 2

I just had the same problem, which I solved with parent/child contexts. Here is the problem I had.

I was updating my Core Data object graph in a background thread which had its own managedObjectContext (which is mandatory), and my fetchedResultsController was not able to pick up the changes done to the database.

After I solved it I took some notes:

ManagedObjectContexts are not thread safe, which means that a managedObjectContext cannot be shared with other threads. If a thread needs to use a managedObjectContext, then it will initialize its own managedObjectContext.

To initialize a managedObjectContext there are two ways:

  • alloc/init then set its persistentStoreCoordinator property

  • alloc/init then set a parentContext property instead of a persistentStoreCoordinator property

note: one cannot set both the persistentStoreCoordinator and the parentContext property of a managedObjectContext.

Using parent/child contexts is needed when the a context is run on a background thread that is "linked to controllers and UI objects that are required to be used only on the main thread"(core data documentation).

Here are the requirements needed for the parent/child contexts:

  • the parent context lives in the main thread

  • the child context lives in the background thread

  • both contexts need to be initialized with an initWithConcurrencyType:NSMainQueueConcurrencyType method.

  • when the batch of changes has been done in the child context, both contexts need to perform a save operation. These save operations need to be nested in performBlock methods, i.e:

    childContext performBlock:^{
        [childContext save:nil];
        [self.parentContext performBlock:^{
            [self.parentContext save:nil];                
        }];
    }];
    

EDIT: The code above is actually a bad idea for 2 reasons:

1) It works without saving the parent context.

2) The main thread is blocked if the parent context runs on it.

I hope it helped !

EDIT: Here is a stackoverflow thread which helped me greatly: Does a Core Data parent ManagedObjectContext need to share a concurrency type with the child context?

Share:
10,309

Related videos on Youtube

Lorenzo B
Author by

Lorenzo B

Software Engineer | PhD in Computer Science

Updated on January 02, 2020

Comments

  • Lorenzo B
    Lorenzo B over 4 years

    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.

    • Artur Friesen
      Artur Friesen over 10 years
      See my answer for this Question. I think this is could help: stackoverflow.com/questions/3923826/…
    • Sjors Provoost
      Sjors Provoost almost 10 years
      From reading your question I think @ArturFriesen is right: NSFetchedResultsController ignores changes which happen outside it's own or it's child context. You'll need to call refreshObject on every relevant object in the main moc. Another method to do that: stackoverflow.com/a/21378533/313633
  • Alexsander Akers
    Alexsander Akers about 12 years
    Many frameworks use Core Data internally. Make sure you don't merge those notifications by mistake.
  • Lorenzo B
    Lorenzo B about 12 years
    @JodyHagins First of all thank you for your answer. + 1 for your support. What I wrote in the edit, I think, it's not a problem. Maybe I've changed something but I don't know what. Could you explain what do you mean with ...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.? Thank you in advance.
  • Lorenzo B
    Lorenzo B about 12 years
    @AlexsanderAkers Thank you for your comment.
  • Jody Hagins
    Jody Hagins about 12 years
    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. Also, please address the other issues I raised (especially the missing sort descriptor).
  • Lorenzo B
    Lorenzo B about 12 years
    @JodyHagins Thank you Jody. I added an edit (Edit 2). Cheers.
  • Jody Hagins
    Jody Hagins about 12 years
    Using CoreData can be so complex, when you have a problem, you almost have to provide all code because so many things are tightly coupled. I would set a breakpoint (or NSLog) right before the save, and dump the entire moc that is being saved (I actually have helper code to do this it is so common for me). My canonical debugging process for CoreData: I have subclasses for NSManagedObjectContext and NSPersistentStoreCoordinator that just NSLog and call super. I also register for every notification, and NSLog them. I suggest you do something similar to get a hand on what is really happening.
  • Lorenzo B
    Lorenzo B about 12 years
    @JodyHagins I'll investigate a bit. About what I wrote 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? Thank you for your support.
  • Lorenzo B
    Lorenzo B over 11 years
    thanks for sharing +1, I don't understand both contexts need to be initialized with an initWithConcurrencyType:NSMainQueueConcurrencyType method.. What does it mean?
  • skozin
    skozin about 11 years
    both contexts need to be initialized with an initWithConcurrencyType:NSMainQueueConcurrencyType method — this is wrong.