iOS - NSFetchedResultsController example with CoreData manipulation through an NSOperation

Jeroen Leenarts

Recently I started working for Xebia and what better way to introduce myself with a nice blogpost and some free code and some explanation to go along with it.

To get you started: here is the code. It's on GitHub so don't be afraid to send in suggestions and whatnot.The example I will be talking about today is mostly contained within these classes:

  • XSDAppDelegate
  • XSDFirstViewController

Just make sure to browse the source code a bit before asking questions. 😉

The following picture should give you a general overview of how the example works.

Ios xsdcontroller1 concept sketch

Now for the interesting bits. As can be seen in the picture the example utilizes a number of mechanisms available in the iOS framework. These are CoreData, NSOperation, NSFetchedResultsController and Notification Center.

The NSFetchedResultsController performs a fetch request on the CoreData store. Any changes affecting the NSFetchedResultsControllers NSManagedObjectContext are picked up and delegated to the XSDFirstViewController. The XSDFirstViewController implements the NSFetchedResultsControllerDelegate protocol.

Here's the implementation of the NSFetchedResultsControllerDelegate. As you can see, nothing too fancy and basically straight from Apple sample code.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    //Lets the tableview know we're potentially doing a bunch of updates.
    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    //We're finished updating the tableview's data.
    [self.tableView endUpdates];
}

- (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:UITableViewRowAnimationAutomatic];
            break;

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

        case NSFetchedResultsChangeUpdate:
            [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

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

}

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

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

The NSOperation subclass XSDSyncOperation defines the background work which alters the CoreDate store contents. Key thing to note is that the XSDSyncOperation obtains it's own NSManagedObjectContext whis uses the same NSPersistentStoreCoordinator as the context used by the NSFetchedResultsController. (Now read that last line again and make sure you understand what I'm saying.)

An NSPersistentStoreCoordinator can be shared across threads, an NSManagedObjectContext can not. Read the CoreData programming guide for more details on this.

Now when the XSDSyncOperation saves something to the CoreDate store, the other context needs to be informed about this change. And that's where the following code snippet comes in, it's part of the XSDSyncOperation.

 //Store resulting objects and broadcast the save operation to the syncdelegate.
 [[NSNotificationCenter defaultCenter] addObserver:syncDelegate selector:@selector(syncDidSave:) name:NSManagedObjectContextDidSaveNotification object:context];

 [context save:&error];
 [[NSNotificationCenter defaultCenter] removeObserver:syncDelegate name:NSManagedObjectContextDidSaveNotification object:context];

There are a few things to note about the above snippet. First the NSManagedObjectContextDidSaveNotification is a notification every NSManagedObjectContext emits when it receives a save message. Second, the observer syncDelegate with the selector syncDidSave:. The XSDAppDelegate implements a protocol called XSDSyncDelegate. This protocol defines the syncDidSave: message. So when the XSDSyncOperation saves its context, a message syncDidSave: is called on the XSDAppDelegate with one parameter, the NSManagedObjectContextDidSaveNotification triggered by the original save message on the XSDSyncOperation's context.

Here's the implementation of the syncDidSave: method on the XSDAppDelegate:

- (void)syncDidSave:(NSNotification *)saveNotification {
 if ([NSThread isMainThread]) {
 [self.managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
 } else {
 [self performSelectorOnMainThread:@selector(syncDidSave:) withObject:saveNotification waitUntilDone:NO];
 }
}

There's a little bit of logic in play here to switch the processing to the main thread. Once we're on the main thread the notification is "merged into the main thread's context. And voila, the context changes, the NSFetchedResultController picks it up and the UI is updated.

Comments (4)

  1. fwielstra - Reply

    February 13, 2012 at 11:05 am

    So if I get this right, this will work properly even in a multithreaded application? Asking this because we had a major issue with our app, Core Data and background tasks executing. Basically, while we were updating our internal data, a background event (didRegisterForRemoteNotifications) would be called which indirectly used the internal data, which led to a crash due to the managed object being used in two separate threads (I believe). We found it a difficult problem to solve, so we simply removed core data and gained a 10x performance increase.

  2. Jeroen Leenarts - Reply

    February 13, 2012 at 11:55 am

    I did notice a case where the App would crash when I would not have any "latency" between the CoreData updates from the background threads.

    When I have spare time I will attempt to test if my current solution is stable or not.

  3. Jeroen Leenarts - Reply

    February 14, 2012 at 2:56 pm

    This commit seems to fix the problem I my previous comment:

    https://github.com/xebia/ios-DemoForBlog/commit/5918448795b2fe0f3c0007c47d18c0a47bbae8a6

  4. [...] iOS – NSFetchedResultsController example with CoreData manipulation through an NSOperation | Xebia... [...]

Add a Comment