NSFetchedResultsController: Fetch in a background thread

burki picture burki · Feb 10, 2013 · Viewed 9.4k times · Source

I have a more or less basic UITableViewController with a NSFetchedResultsController. The UITableViewController is pushed onto the navigationController's stack. But the push animation isn't smooth because the fetch of NSFetchedResultsController is performed on the main thread, and therefore blocks the UI.

My question is: How can I perform the fetch of the NSFetchedResultsController in a background thread to keep the animation smooth?

The NSFetchedResultsController and the delegate methods look like this:

- (NSFetchedResultsController *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"GPGrade" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    //Set predicate
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"parent == %@", self.subject];
    [fetchRequest setPredicate:predicate];


    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = @[sortDescriptor];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"SubjectMaster"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Replace this implementation with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return _fetchedResultsController;
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}

- (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;
    }

}

- (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:@[newIndexPath] withRowAnimation:UITableViewRowAnimationTop];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
            break;

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

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

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

Answer

quellish picture quellish · Jul 29, 2014

TL;DR; There is no good reason to use a context on the main queue.

can use NSFetchedResultsController to fetch data in background

Absolutely. NSFetchedResultsController can be used with a private queue context. It is, in fact, quite happy and performant when doing so. There is a bug that prevents NSFetchedResultsController from using it's cache when it's using a private queue, but the cache does not win you as much as it did in iOS 3.0. Set a cacheName of nil and you will be fine.

1. Create a context with NSPrivateQueueConcurrencyType. Preferably not the one you use for IO.

2. Create the fetched results controller with that context, and a cache name of nil.

3. Perform your initial fetch from within a performBlock: block:

 [[[self fetchedResultsController] managedObjectContext] performBlock:^{
    NSError *fetchError = nil;
    if (![self fetchedResultsController] performFetch:&error]){
        /// handle the error. Don't just log it.
    } else {
        // Update the view from the main queue.
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [tableView reloadData];
         }];
    }
 }];

4. All of your delegate callbacks will now happen from the context's queue. If you are using them to update views, do so by dispatching to the main queue like you see above.

5. ...

6. Profit!

You can read more about this here.