2

I'm using NSFetchedResultsController with sortDescriptors on the request to populate a table with a lot of results in it. I notice that when a change occurs that moves a row from near the bottom of the table to the top, didChangeObject:atIndexPath:forChangeType:newIndexPath: is not called at all.

Strangely, I can work around this by iterating through all the fetched objects and accessing any attribute on them right after calling performFetch.

Any tips on what the problem might be, or is this just an obscure Apple bug?

Here is my code:

NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:@"MyObject" inManagedObjectContext:context];
request.sortDescriptors = @[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:NO]];
request.fetchBatchSize = 20;
NSFetchedResultsController *fetched = [[NSFetchedResultsController alloc]
                                       initWithFetchRequest:request
                                       managedObjectContext:context
                                         sectionNameKeyPath:nil
                                                  cacheName:nil];
fetched.delegate = self;
NSError *error = nil;
if (![fetched performFetch:&error]) {
    NSLog(@"Unresolved error fetching objects: %@", error);
}

// Should not be necessary, but objects near the bottom won't move to the top without it.
for (MyObject *o in fetched.fetchedObjects) {
    o.someAttribute;
}

Updated September 12, 2014:

I'm saving all data in a background managed object context, and it seems to be related to the issues I'm seeing. Here is my code for merging changes from to the main object context:

+(void)initSaveListener {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:[self privateContext]];
}

+(void)mergeChanges:(NSNotification*)notification {
    NSManagedObjectContext *context = [self mainContext];

    [context performBlock:^{
        [context mergeChangesFromContextDidSaveNotification:notification];
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"error merging changes %@, %@", error, [error userInfo]);
        }
    }];
}
ninjudd
  • 553
  • 4
  • 11
  • What is causing the move? From your description, it sounds like the change is happening in the table view (i.e. the user is moving a row) – quellish Aug 31 '14 at 06:00
  • A user action is not causing it to move. The change happens on the server and is streamed to the client and updated in Core Data. This part is working correctly, and calling reloadData on the table view causes the correct order to be displayed. – ninjudd Sep 08 '14 at 17:02
  • is that Core Data change happening in the context described by `[self managedObjectContext]`, a parent context, or a child context? – quellish Sep 08 '14 at 18:28
  • @quellish you're right. This does seem to be related to the fact that my changes are happening in a background context and being propagated to the main context through `NSManagedObjectContextDidSaveNotification`. – ninjudd Sep 12 '14 at 19:17
  • Can you update your question with the method that handles the merge notification? It's likely that's the problem, or where you are setting up the notification observation – quellish Sep 12 '14 at 21:18

2 Answers2

2

It turns out that this problem was caused by the fact that the changes to managedObjectContext were being propagated from another context using NSManagedObjectContextDidSaveNotification. This blog post explains in detail why this causes a problem for NSFetchedResultsController, and how to fix it:

http://www.mlsite.net/blog/?p=518

Here is the specific fix in the context of my code above:

+(void)mergeChanges:(NSNotification*)notification {
    NSManagedObjectContext *context = [self mainContext];

    // Fault all objects that have changed so that NSFetchedResultsController will see the changes.
    NSArray *objects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
    for (NSManagedObject *object in objects) {
        [[context objectWithID:[object objectID]] willAccessValueForKey:nil];
    }

    [context performBlock:^{
        [context mergeChangesFromContextDidSaveNotification:notification];
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"error merging changes %@, %@", error, [error userInfo]);
        }
    }];
}
ninjudd
  • 553
  • 4
  • 11
0

You are creating a brand new fetched results controller. So there was no change (like insert, delete, update), so the delegate is not called. There are two ways to deal with this.

First, you could use the existing FRC and just change the predicate of its fetch request (the fetch request itself is readonly). Then you just call performFetch. Depending on your needs this could be sufficient.

Second, if you need to wipe the FRC and create a new one, you need to call reloadData on the table view. I usually do this by changing the logic of FRC creation via some ivars (following the Apple template, the FRC is created lazily), and just set the FRC to nil and call [self.tableView reloadData];.

Mundi
  • 79,884
  • 17
  • 117
  • 140
  • the code he posted is how he creates the fetched results controller. If you read his question, it's pretty clear that he is doing a fetch with that fetched results controller, and expects to see the delegate callbacks (but isn't, at least for a move). A fetched results controller uses `performFetch:` to populate it's fetched objects, and subsequently listens for changes to the context that affect objects matching it's fetch request - this is what triggers the delegate callbacks. – quellish Sep 01 '14 at 04:21
  • @quellish I understand what you are trying to say. That is why I gave the recommendations in my answer. – Mundi Sep 01 '14 at 06:14
  • The predicate of the FRC isn't changing. Only the value of the sort field of the underlying data is changing. – ninjudd Sep 08 '14 at 15:57
  • That amounts to a new predicate, no? – Mundi Sep 08 '14 at 16:18
  • No. The predicate is identical. – ninjudd Sep 10 '14 at 05:45