Search code examples
iosobjective-cuitableviewcore-datansfetchedresultscontroller

NSFetchResultsController shows spurious objects


I have a UITableView showing objects from a Core Data entity that are received from a WebServer, through a NSFetchResultController. The user can modify them and send them back to the server. She also can tap a button to refresh those objects from the server.

Each object has an identifier attribute. When I receive an object JSON from the server, I look for an existing object with that same identifier. It it exists, I update it. Otherwise I create it.

Some of this is happening with a Main Queue NSManagedObjectContext, some of it in a child Private Queue one. In all cases, it happens in a performBlock method and both the child context and its parent is saved.

This sounds like bread and butter Core Data patterns. Now my issue:

Sometimes, after a server refresh, the NSFetchResultController shows two instances of the same object. The two copies are distinct (their pointers are different). One copy is complete, the other only has its attribute values set, not its relations. Both have the same NSManagedObjectContext. Both have the same identifier.

How can I debug such an issue? I checked that my CoreData store does not have two instances of the same object (by looking inside the SQLite file, and also by putting a symbolic breakpoint on awakeFromInsert). I traced through the code that looks for the existing instance and it finds it alright.

At this point, I am stuck, and I have a hard time imagining a debugging strategy.

I can provide all the details imaginable, but beside showing the full source code, I am not sure what would be the most useful.

Thanks for any help.

JD

Edit 1: Here is my controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:(DaySlotCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

Edit 2: here is how my contexts are arranged. I have a central singleton model object that takes care of communication with the remote server (thus its class name SGIServer). It holds two contexts:

  • mainManagedObjectContext, a NSMainQueueConcurrencyType, is used for all UI-related stuff, included the NSFetchResultController described above (even though I read on the net that an NSFetchResultController can use a private context). It is not associated with the persistent store. It is a child of:

  • persistentManagedObjectContext, a NSPrivateQueueConcurrencyType, associated with the persistent store, in charge of saving to the store in the background:

They are created at launch time like this:

NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
self.persistentManagedObjectContext = managedObjectContext;
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
managedObjectContext.parentContext = self.persistentManagedObjectContext;
self.mainManagedObjectContext = managedObjectContext;

Code that needs a context do so in two different ways depending if they want the main context or not:

NSManagedObjectContext *moc = [server mainManagedObjectContext];

or

NSManagedObjectContext *moc = [server newPrivateContext];

where newPrivateContext simply creates a new NSPrivateQueueConcurrencyType context, child of the main one:

- (NSManagedObjectContext *) newPrivateContext
{
    NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    privateContext.parentContext = self.mainManagedObjectContext;
    return privateContext;
}

Finally, I defined two save methods, a synchronous one and an asynchronous one:

- (void)syncSaveContext: (NSManagedObjectContext *) moc persisting:(BOOL)saveToDisk
{
    NSManagedObjectContext *mainContext = self.mainManagedObjectContext;

    if (moc && moc != mainContext) {
        NSError *error = nil;
        if (![moc save:&error]) {
            NSLog(@"Error saving MOC: %@\n%@",[error localizedDescription], [error userInfo]);
        }
    }

    if (mainContext && [mainContext hasChanges]) {
        [mainContext performBlockAndWait:^{
            NSError *error = nil;
            if (![mainContext save:&error]) {
                NSLog(@"Error saving MOC: %@\n%@",[error localizedDescription], [error userInfo]);
            }
        }];
    }
    if (saveToDisk) {
        NSManagedObjectContext *privateContext = self.persistentManagedObjectContext;

        if (privateContext && [privateContext hasChanges]) {
            [privateContext performBlockAndWait: ^{
                NSError *error = nil;
                if (![privateContext save:&error]) {
                    NSLog(@"Error saving private MOC: %@\n%@",[error localizedDescription], [error userInfo]);
                }
            }];
        }
    }
}

and:

- (void)asyncSaveContext: (NSManagedObjectContext *) moc persisting:(BOOL)saveToDisk
{
    NSManagedObjectContext *mainContext = self.mainManagedObjectContext;

    if (moc && moc != mainContext) {
        NSError *error = nil;
        if (![moc save:&error]) {
            NSLog(@"Error saving MOC: %@\n%@",[error localizedDescription], [error userInfo]);
        }
    }

    if (mainContext && [mainContext hasChanges]) {
        [mainContext performBlock:^{
            NSError *error = nil;
            if ([mainContext save:&error]) {
                if (saveToDisk) {
                    NSManagedObjectContext *privateContext = self.persistentManagedObjectContext;

                    if (privateContext && [privateContext hasChanges]) {
                        [privateContext performBlock: ^{
                            NSError *error = nil;
                            if (![privateContext save:&error]) {
                                NSLog(@"Error saving private MOC: %@\n%@",[error localizedDescription], [error userInfo]);
                            }
                        }];
                    }
                }
            } else {
                NSLog(@"Error saving MOC: %@\n%@",[error localizedDescription], [error userInfo]);
            }
        }];
    }
}

The async one is the most used one, typically at the end of any user-triggered action. The sync one is occasionally used when I want to make sure the save has been done before I continue.


Solution

  • If you have some kind of two context set up (parent and child, main queue and private queue) and this tends to happen after calling save on your contexts then you may have a similar problem to something I had where temporary objects are leaking in to your context. As far as I'm aware this is a bug in core data

    Where you call save, try calling obtainPermanentIDsForObjects inside your parent context perform block like this:

    [self.parentContext performBlockAndWait:^{
                NSError * error = nil;
                [self.parentContext obtainPermanentIDsForObjects:[self.parentContext.insertedObjects allObjects] error:&error];
                [self.parentContext save: &error]
            }];