Episode #69

NSFetchedResultsController

18 minutes
Published on May 30, 2013

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we take a look at how to correctly detect & respond to underlying changes in an NSManagedObjectContext and insert, update, move, and remove UITableView rows with the proper animations.

Episode Links

Setup

Set up the NSManagedObjectContext and the NSFetchedResultsController with a fetch request and set the delegate to self.

- (void)viewDidLoad {
    [super viewDidLoad];

    self.title = @"Oil Changes";
    [self listenForChanges];

    self.context = [[OILDataModel shareDataModel] mainContext];
    [self setupFetchedResultsController];
}

- (NSFetchRequest *)fetchRequest {
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entry"];
    NSSortDescriptor *sortByDate = [NSSortDescriptor sortDescriptorWithKey:@"date" ascending:NO];
    [fetchRequest setSortDescriptors:@[sortByDate]];
    return fetchRequest;
}

- (void)setupFetchedResultsController {
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:[self fetchRequest]
                                                                        managedObjectContext:self.context
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
    self.fetchedResultsController.delegate = self;
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
        NSLog(@"ERROR: %@", error);
        [[[UIAlertView alloc] initWithTitle:@"Error"
                                    message:@"Couldn't fetch entries :("
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
}

UITableView implementation

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id<NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"entryCell";
    OILEntryCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    OILEntry *entry = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [self configureCell:cell forEntry:entry];

    return cell;
}

- (void)configureCell:(OILEntryCell *)cell forEntry:(OILEntry *)entry {
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.dateLabel.text = [self.dateFormatter stringFromDate:entry.date];
    cell.milesLabel.text = [NSString stringWithFormat:@"%@", entry.miles];
    cell.logLabel.text = entry.log;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    OILEntry *entry = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [self.context deleteObject:entry];
    [self.context performBlock:^{
        NSError *error = nil;
        if (![self.context save:&error]) {
            NSLog(@"Couldn't delete entry: %@", error);
            [[[UIAlertView alloc] initWithTitle:@"ERROR"
                                        message:@"Couldn't delete entry"
                                       delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil] show];
        }
    }];
}

-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellEditingStyleDelete;
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

Listening for changes

When another context saves data, we need to merge in those changes:

- (void)listenForChanges {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(contextDidSave:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:nil];
}

- (void)contextDidSave:(NSNotification *)notification {
    NSLog(@"Merging changes...");
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.context mergeChangesFromContextDidSaveNotification:notification];
    });
}

Updating the table

In episode 12 I just executed the fetch request again and reloaded the whole table. This was a short-cut and isn't very polished. It's not easy to tell which row was updated when doing this. Instead, we can respond to the NSFetchedResultsControllerDelegate callbacks:

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

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeUpdate:
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

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

We haven't handled the case of moves, but this could be the topic of a future episode.