NSFetchedResultsController does not always call didChangeObject: atIndexPath: forChangeType: newIndexPath: for NSFetchedResultsChangeMove

I am using NSFetchedResultsController

c sortDescriptors

in a query to populate a table with many results. I notice that when a change occurs that moves a row from the bottom of the table to the top, didChangeObject:atIndexPath:forChangeType:newIndexPath:

it is not called at all.

The weird thing is, I can get around this by iterating over all the selected objects and accessing any attribute on them right after the call performFetch

.

Any advice on what might be the problem, 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 am storing all data in the context of a background managed object and seems to be related to the problems I see. Here is my code to merge changes into the main context of an object:

+(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]);
        }
    }];
}

      

+2


source to share


2 answers


It turns out that this problem was caused by the changes to being managedObjectContext

propagated from a different context using NSManagedObjectContextDidSaveNotification

. This blog post explains in detail why this is causing the problem for NSFetchedResultsController

and how to fix it:

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



Here's a 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]);
        }
    }];
}

      

+2


source


You are creating a completely new results controller. So there was no change (e.g. insert, delete, update), so no delegate is called. There are two ways to handle this.

You can first use an existing FRC and just change the predicate of its fetch query (the fetch query itself readonly

). Then you just call performFetch

. This may be sufficient depending on your needs.



Second, if you need to erase the FRC and create a new one, you need to call reloadData

on the table view. I usually do this by changing the FRC creation logic through some ivars (after Apple template, FRC is lazy created) and just set FRC to nil

and call [self.tableView reloadData];

.

0


source







All Articles