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 Episode Source Code NSFetchedResultsController Class Reference 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.