Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSFetchedResultsController doesn't see new inserts / removes fetched values after update

After reading dozens of similar questions, I'd like to start with this statement: "I DID set the delegate of the NSFetchedResultsController, it didn't work.".

So I have a simple TableViewController whose cells are filled with NSFetchedResultsController. Here's the FRC init code:

@property (strong, nonatomic) NSFetchedResultsController *frc;

...

- (NSFetchedResultsController *)frc
{
    if (!_frc)
    {
        NSError *error = nil;
        NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:e_product];
        request.sortDescriptors = [NSArray arrayWithObjects:
                               [NSSortDescriptor sortDescriptorWithKey:@"product_group.product_group_name" ascending:YES],
                               [NSSortDescriptor sortDescriptorWithKey:f_product_name ascending:YES],
                               nil];
        _frc = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[DTGGlobalSettings sharedInstance].moc sectionNameKeyPath:@"product_group.product_group_name" cacheName:nil];
        _frc.delegate = self;
        [_frc performFetch:&error];
        if (error)
        {
            NSLog(@"Error while fetching products: %@!", error.userInfo);
        }
    }

    return _frc;
}

I also have NSFetchedResultsController delegate methods implemented with some debugging labels:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    NSLog(@"controllerWillChangeContent");
    [self.tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

switch(type) {

    case NSFetchedResultsChangeInsert:
        NSLog(@"didChangeObject - insert");
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        NSLog(@"didChangeObject - delete");
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeUpdate:
        NSLog(@"didChangeObject - update");
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
        break;

    case NSFetchedResultsChangeMove:
        NSLog(@"didChangeObject - move");
        [tableView deleteRowsAtIndexPaths:[NSArray
                                           arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [tableView insertRowsAtIndexPaths:[NSArray
                                           arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;
}
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

switch(type) {

    case NSFetchedResultsChangeInsert:
        NSLog(@"didChangeSection - insert");
        [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        NSLog(@"didChangeSection - delete");
        [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;
}
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
NSLog(@"controllerDidChangeContent");
[self.tableView endUpdates];
}

In my navigationBar I have the refresh button, its function is to fire up the product catalog method who does the following: 1. Fetches products from remote MySQL server 2. If the product with ID doesn't exists - creates it and fills all the fields 3. If it exists, all the fields are updated according to the fetched values

After row 2 and 3 the store is saved. I use single NSManagedObjectContext for all CoreData operations across the app.

When I start the app for the first time, TVC is empty, because there was no products fetched yet. When I press Refresh, the new products are fetched and inserted into ManagedObjectContext, but NSManagedObjectContext doesn't see/hear any changes: no delegate methods are called, so no new rows are added to TVC. When I restart the app the new products are in place fetched into TVC with no problem.

If I press refresh button once again when there are some products there, after short delay they all disappear. Some debugging showed me that if happens after updating the fields of the entities (actually values are the same) and saving the store. Delegate methods this time work like a charm and for every updated and saved entity controller:didChangeObject:atIndexPath:forChangeType:newIndexPath is called with forChangeType = NSFetchedResultsChangeDelete.

Question #1: why NSFetchedResultsController doesn't see inserted entities without app restart? Question #2: why NSFetchedResultsController marks already fetched entities as "non existing" (?) and removes them from TVC after store save?

I would appreciate any help and ideas, being fighting with this issue for whole last week ;(

UPD1 (for Jody): Here are some details. I use custom class DTGGlobalSettings to share some vars across the app. That's how I init its sharedInstance property:

+(DTGGlobalSettings *)sharedInstance
{
    static DTGGlobalSettings *myInstance = nil;

    if (nil == myInstance)
    {
        myInstance = [[[self class] alloc] init];
        // set values here
        // try to acces MOC to init CoreData 
        [myInstance moc];        
        myInstance.baseURL = @"http://local.app/";
}
return myInstance;
}

To init CoreData stack I used an example from Apple documentation, because for some reason I don't see the checkbox "Use CoreData" when creating a new project. I'm sure that I've seen it before in Xcode, but now I don't ;( (Xcode 4.4.1):

#pragma mark Core Data stack

- (NSManagedObjectContext *) moc {

if (_moc != nil) {
    return _moc;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
    _moc = [[NSManagedObjectContext alloc] init];
    [_moc setPersistentStoreCoordinator: coordinator];
}
return _moc;
}


- (NSManagedObjectModel *)managedObjectModel {

if (_managedObjectModel != nil) {
    return _managedObjectModel;
}
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];    
return _managedObjectModel;
}


- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

if (_persistentStoreCoordinator != nil) {
    return _persistentStoreCoordinator;
}
NSURL *storeUrl = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
storeUrl = [storeUrl URLByAppendingPathComponent:DOC_NAME];

NSError *error;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];

if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
    NSLog(@"Error adding a store to the coordinator: %@, %@", error, error.userInfo);
}        
return _persistentStoreCoordinator;
}

-(void)saveDataStore
{
    NSError *error;
    if (![self.moc save:&error]) NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}

The 'moc' (ManagedObjectContext) property of DTGGlobalSettings is then used everywhere in the app where I need access to CoreData.

Now to the second part, updating the database. I'm getting some new entities in JSON format from remote web-server, going through the resulting dictionary and calling createFromDictionary method for each entity found in request results. Here's the code:

+(Product *)createFromDictionary:(NSDictionary *)entityData inContext:(NSManagedObjectContext *)moc performUpdate:(BOOL)update
{
Product *result;
if (update)
{
    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:e_product];
    request.predicate = [NSPredicate predicateWithFormat:@"%K = %@", f_product_id, [entityData objectForKey:f_product_id]];
    result = [[DTGGlobalSettings sharedInstance].moc executeFetchRequest:request error:nil].lastObject;
    if (!result)
    {
        NSLog(@"Error updating Product ID %@! Cannot fetch entity.", [entityData objectForKey:f_product_id]);
        return nil;
    }
} else
{
    result = [NSEntityDescription insertNewObjectForEntityForName:e_product inManagedObjectContext:[DTGGlobalSettings sharedInstance].moc];
}

result.product_id = [[DTGGlobalSettings sharedInstance].numFormatter numberFromString:[entityData objectForKey:f_product_id]];
result.product_image = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[DTGGlobalSettings sharedInstance].baseURL stringByAppendingString:[entityData objectForKey:f_product_image]]]];
result.product_name = [entityData objectForKey:f_product_name];

return result;
}

When I run out of fethed entities, I call [[DTGGlobalSettings sharedInstance] saveDataStore] (see above). At this point the existing rows in TVC (if there were any) disappearing. In case if I was adding some new entities, at the point of save nothing happens, i.e. TVC doesn't update its rows.

like image 904
Arseniy Avatar asked Sep 14 '12 20:09

Arseniy


2 Answers

Actually, I think your first pre-edit posting had all the necessary information. I just did not read it (I don't like having to keep scrolling right read long lines of code and sometimes just don't do it - my bad).

Your issue seems to be related to your FRC sort descriptors.

FRC does not deal well with keeping relationship sort descriptors updated very well.

Some have argued that it is how it's designed. I argue it is a bug.

See this post Changing a managed object property doesn't trigger NSFetchedResultsController to update the table view for my take. Hopefully, you will see the similarities to your case.

like image 59
Jody Hagins Avatar answered Sep 21 '22 03:09

Jody Hagins


I just spent time with the same issue and found out the following: when the sectionNameKeyPath was referring to a relationship field that could take the nil value, the FRC update would not work when inserting objects.

This may be related to a warning in Xcode console saying that the section would be created for objects with that field set to nil. I guess that section messes up with the FRC inner-working.

like image 33
harrouet Avatar answered Sep 21 '22 03:09

harrouet