14
  1. I populate and save: an initial NSManagedObjectContext
  2. setup an NSFetchedResultsController with a different NSManagedObjectContext, which filters on a boolean "show" attribute.
  3. Finally update "show" on yet another NSManagedObjectContext and save:.

I expect that this should cause my NSFetchedResultsController to call NSFetchedResultsControllerDelegate's controllerDidChangeContent:. I never get that call. NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext's accepted answer indicates that in addition to controllerDidChangeContent:, I should get an NSManagedObjectContextObjectsDidChangeNotification, but I don't receive that either.

A complete code example is included below and on github. I've filed a radar with Apple.

@interface HJBFoo : NSManagedObject

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *show;

@end

@interface HJBAppDelegate () <NSFetchedResultsControllerDelegate>

@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (nonatomic, strong) NSManagedObjectContext *initialManagedObjectContext;
@property (nonatomic, strong) NSManagedObjectContext *fetchedResultsControllerManagedObjectContext;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;

@end

@implementation HJBAppDelegate

#pragma mark - UIApplicationDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = [UIViewController new];

    NSAttributeDescription *nameAttributeDescription = [NSAttributeDescription new];
    [nameAttributeDescription setAttributeType:NSStringAttributeType];
    [nameAttributeDescription setIndexed:NO];
    [nameAttributeDescription setOptional:NO];
    [nameAttributeDescription setName:@"name"];

    NSAttributeDescription *showAttributeDescription = [NSAttributeDescription new];
    [showAttributeDescription setAttributeType:NSBooleanAttributeType];
    [showAttributeDescription setIndexed:YES];
    [showAttributeDescription setOptional:NO];
    [showAttributeDescription setName:@"show"];

    NSEntityDescription *fooEntityDescription = [NSEntityDescription new];
    [fooEntityDescription setManagedObjectClassName:@"HJBFoo"];
    [fooEntityDescription setName:@"HJBFoo"];
    [fooEntityDescription setProperties:@[
     nameAttributeDescription,
     showAttributeDescription,
     ]];

    NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel new];
    [managedObjectModel setEntities:@[
     fooEntityDescription,
     ]];

    self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
    NSError *error = nil;
    if ([self.persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
                                                      configuration:nil
                                                                URL:nil
                                                            options:nil
                                                              error:&error]) {
        self.initialManagedObjectContext = [NSManagedObjectContext new];
        [self.initialManagedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];

        HJBFoo *foo1 = [NSEntityDescription insertNewObjectForEntityForName:@"HJBFoo"
                                                     inManagedObjectContext:self.initialManagedObjectContext];
        foo1.name = @"1";
        foo1.show = @YES;

        HJBFoo *foo2 = [NSEntityDescription insertNewObjectForEntityForName:@"HJBFoo"
                                                     inManagedObjectContext:self.initialManagedObjectContext];
        foo2.name = @"2";
        foo2.show = @NO;

        error = nil;
        if ([self.initialManagedObjectContext save:&error]) {
            NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"HJBFoo"];
            [fetchRequest setReturnsObjectsAsFaults:NO];

            error = nil;
            NSArray *initialFoos = [self.initialManagedObjectContext executeFetchRequest:fetchRequest
                                                                                   error:&error];
            if (initialFoos) {
                NSLog(@"Initial: %@", initialFoos);

                self.fetchedResultsControllerManagedObjectContext = [NSManagedObjectContext new];
                [self.fetchedResultsControllerManagedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];

                NSFetchRequest *shownFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"HJBFoo"];
                [shownFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"show == YES"]];
                [shownFetchRequest setSortDescriptors:@[
                 [NSSortDescriptor sortDescriptorWithKey:@"name"
                                               ascending:YES],
                 ]];

                self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:shownFetchRequest
                                                                                    managedObjectContext:self.fetchedResultsControllerManagedObjectContext
                                                                                      sectionNameKeyPath:nil
                                                                                               cacheName:nil];
                self.fetchedResultsController.delegate = self;
                error = nil;
                if ([self.fetchedResultsController performFetch:&error]) {
                    NSLog(@"Initial fetchedObjects: %@", [self.fetchedResultsController fetchedObjects]);

                    [[NSNotificationCenter defaultCenter] addObserver:self
                                                             selector:@selector(managedObjectContextDidSave:)
                                                                 name:NSManagedObjectContextDidSaveNotification
                                                               object:nil];
                    [[NSNotificationCenter defaultCenter] addObserver:self
                                                             selector:@selector(managedObjectContext2ObjectsDidChange:)
                                                                 name:NSManagedObjectContextObjectsDidChangeNotification
                                                               object:self.fetchedResultsControllerManagedObjectContext];

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC),
                                   dispatch_get_main_queue(),
                                   ^(void){
                                       NSManagedObjectContext *managedObjectContext3 = [NSManagedObjectContext new];
                                       [managedObjectContext3 setPersistentStoreCoordinator:self.persistentStoreCoordinator];

                                       NSFetchRequest *foo2FetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"HJBFoo"];
                                       [foo2FetchRequest setFetchLimit:1];
                                       [foo2FetchRequest setPredicate:[NSPredicate predicateWithFormat:@"name == %@",
                                                                       @"2"]];
                                       NSError *editingError = nil;
                                       NSArray *editingFoos = [managedObjectContext3 executeFetchRequest:foo2FetchRequest
                                                                                                   error:&editingError];
                                       if (editingFoos) {
                                           HJBFoo *editingFoo2 = [editingFoos objectAtIndex:0];
                                           editingFoo2.show = @YES;

                                           editingError = nil;
                                           if ([managedObjectContext3 save:&editingError]) {
                                               NSLog(@"Save succeeded. Expected (in order) managedObjectContextDidSave, controllerDidChangeContent, managedObjectContext2ObjectsDidChange");
                                           } else {
                                               NSLog(@"Editing save failed: %@ %@", [error localizedDescription], [error userInfo]);
                                           }
                                       } else {
                                           NSLog(@"Editing fetch failed: %@ %@", [error localizedDescription], [error userInfo]);
                                       }

                                   });
                } else {
                    NSLog(@"Failed initial fetch: %@ %@", [error localizedDescription], [error userInfo]);
                }
            } else {
                NSLog(@"Failed to performFetch: %@ %@", [error localizedDescription], [error userInfo]);
            }
        } else {
            NSLog(@"Failed to save initial state: %@ %@", [error localizedDescription], [error userInfo]);
        }
    } else {
        NSLog(@"Failed to add persistent store: %@ %@", [error localizedDescription], [error userInfo]);
    }

    [self.window makeKeyAndVisible];
    return YES;
}

#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    NSLog(@"controllerDidChangeContent: %@",
          [self.fetchedResultsController fetchedObjects]);
}

#pragma mark - notifications

- (void)managedObjectContextDidSave:(NSNotification *)notification {
    NSManagedObjectContext *managedObjectContext = [notification object];
    if (([managedObjectContext persistentStoreCoordinator] == self.persistentStoreCoordinator) &&
        (managedObjectContext != self.fetchedResultsControllerManagedObjectContext)) {
        NSLog(@"managedObjectContextDidSave: %@", notification);
        [self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    }
}

- (void)managedObjectContext2ObjectsDidChange:(NSNotification *)notification {
    NSLog(@"managedObjectContext2ObjectsDidChange: %@", notification);
}

@end

@implementation HJBFoo

@dynamic name;
@dynamic show;

@end
Community
  • 1
  • 1
Heath Borders
  • 30,998
  • 16
  • 147
  • 256
  • + 1 for your question. I having the same problems as described here http://stackoverflow.com/questions/13527133/needed-clarifications-for-nsfetchedresultscontroller-and-nsfetchedresultscontrol. Furthermore, thanks for sending a radar. – Lorenzo B Dec 24 '12 at 10:32
  • Does the problem remain if you set up the fetched controller with the same context you set up in step 1? – Lorenzo B Dec 24 '12 at 10:36
  • Are you able to see the specific change (from `NO` to `YES`) when you log the notification in `managedObjectContextDidSave` method? – Lorenzo B Dec 24 '12 at 12:47

1 Answers1

19

It seems to me that applying the fix/workaround from NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext solves your problem as well. Your managedObjectContextDidSave method would then look like this:

- (void)managedObjectContextDidSave:(NSNotification *)notification {
    NSManagedObjectContext *managedObjectContext = [notification object];
    if (([managedObjectContext persistentStoreCoordinator] == self.persistentStoreCoordinator) &&
        (managedObjectContext != self.fetchedResultsControllerManagedObjectContext)) {
        NSLog(@"managedObjectContextDidSave: %@", notification);

        // Fix/workaround from https://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different/3927811#3927811
        for(NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey]) {
            [[self.fetchedResultsControllerManagedObjectContext objectWithID:[object objectID]] willAccessValueForKey:nil];
        }

        [self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    }
}

and the NSLog output looks as expected:

Initial: (
    "<HJBFoo: 0xaa19670> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: {\n    name = 1;\n    show = 1;\n})",
    "<HJBFoo: 0xaa1a200> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n    name = 2;\n    show = 0;\n})"
)
Initial fetchedObjects: (
    "<HJBFoo: 0x74613f0> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: <fault>)"
)
managedObjectContextDidSave: NSConcreteNotification 0xaa1f480 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0xaa1ed90>; userInfo = {
    inserted = "{(\n)}";
    updated = "{(\n    <HJBFoo: 0xaa1f2d0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n    name = 2;\n    show = 1;\n})\n)}";
}}
Save succeeded. Expected (in order) managedObjectContextDidSave, controllerDidChangeContent, managedObjectContext2ObjectsDidChange
controllerDidChangeContent: (
    "<HJBFoo: 0x74613f0> (entity: HJBFoo; id: 0xaa1afd0 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p2> ; data: {\n    name = 1;\n    show = 1;\n})",
    "<HJBFoo: 0xaa1f9c0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n    name = 2;\n    show = 1;\n})"
)
managedObjectContext2ObjectsDidChange: NSConcreteNotification 0xaa1fbb0 {name = NSObjectsChangedInManagingContextNotification; object = <NSManagedObjectContext: 0x745fa50>; userInfo = {
    managedObjectContext = "<NSManagedObjectContext: 0x745fa50>";
    refreshed = "{(\n    <HJBFoo: 0xaa1f9c0> (entity: HJBFoo; id: 0xaa1af50 <x-coredata://07E97098-E32D-45F6-9AB4-F9DAB9B0AC1A/HJBFoo/p1> ; data: {\n    name = 2;\n    show = 1;\n})\n)}";
}}

So the following things happen:

  • Changes are made in the "background" context managedObjectContext3 and saved.
  • You receive a NSManagedObjectContextDidSaveNotification and managedObjectContextDidSave: is called. This notification contains the object that was updated in the background context.
  • Calling

    [self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    

    now would not do anything, because the updated object has not been loaded into the fetchedResultsControllerManagedObjectContext or is a fault. In particular, this context would not post a NSManagedObjectContextObjectsDidChangeNotification and the fetched results controller would not update.

  • But calling willAccessValueForKey for the updated objects first forces the fetchedResultsControllerManagedObjectContext to load these objects and fire a fault.
  • Calling

    [self.fetchedResultsControllerManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    

    after that actually updates the objects in the fetchedResultsControllerManagedObjectContext.

  • Therefore a NSManagedObjectContextObjectsDidChangeNotification is posted and the fetched results controller calls the corresponding delegate functions.
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Martin, + 1 for your reply. So, I think this is due to memory-only tracking option as I explained in my previous question at http://stackoverflow.com/questions/13527133/needed-clarifications-for-nsfetchedresultscontroller-and-nsfetchedresultscontrol. What do you think about? – Lorenzo B Dec 24 '12 at 14:20
  • 1
    @flexaddicted: I had a similar issue with a SQLite store, but I haven't checked if this fix helps there as well. I cannot check it today, but will keep you up to date. – Martin R Dec 24 '12 at 15:15
  • Thanks. This does fix my problem, but it didn't in my actual project. Now I need to figure out how to create a better test case to reproduce my bug. – Heath Borders Dec 24 '12 at 19:59
  • 1
    @flexaddicted: This workaround (calling willAccessValueForKey for the updated objects to force the main context to load the objects) solved a similar issue in my project with a SQLite based store. – Martin R Jan 02 '13 at 10:35
  • @MartinR Thanks for your reply. I will set up a dummy project for testing this. Thanks for returning with your reply. So, memory-tracking only is the right track, I suppose. – Lorenzo B Jan 02 '13 at 10:42
  • @flexaddicted: I do not think that the problem is caused by memory-only tracking, because I use that as well. - If you have a dummy project to share, I would be interested to look into it. – Martin R Jan 02 '13 at 10:50
  • @MartinR Do you think it's bug? If an object has some properties as faults, those properties will not be tracked. They are not loaded in memory completely (I suppose this based on doc, but I could be wrong). As soon as possible I would setup that project. – Lorenzo B Jan 02 '13 at 10:57
  • 1
    @flexaddicted: I think it is just as described in http://stackoverflow.com/questions/3923826/nsfetchedresultscontroller-with-predicate-ignores-changes-merged-from-different/3927811#3927811: `mergeChangesFromContextDidSaveNotification` does not update fault objects, even if they were changed in the background context. I cannot say if it is a bug, but it is at least counter-intuitive ... – Martin R Jan 02 '13 at 11:05
  • @MartinR do you know the workaround for problem in this question http://stackoverflow.com/questions/26832627/nsfetchedresultscontroller-didchangeobject-delete-instead-of-update? – Bartłomiej Semańczyk Sep 03 '15 at 16:35