Episode #121

Mantle with Core Data Part 2

16 minutes
Published on May 29, 2014

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

In this episode we continue with our mantle example, this time binding the code to the UI. This involves mapping back to our mantle model for display on the cell, as well as responding to changes using the NSFetchedResultsControllerDelegate protocol.

Episode Links

Implementing NSFetchedResultsController to fetch records from Core Data

- (void)refresh {
    NSError *fetchError;
    if(![self.fetchedResultsController performFetch:&fetchError]) {
        NSLog(@"Couldn't fetch:%@", fetchError);
    }
}

Our fetched results controller is lazily initialized like this:

- (NSFetchedResultsController *)fetchedResultsController {
    if (_fetchedResultsController == nil) {
        NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Episode"];
        fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"publishedAt" ascending:NO] ];
        _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                    managedObjectContext:self.persistenceController.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
    }

    return _fetchedResultsController;
}

Next we just need to use the fetched results controller to tell our table view how many rows it has:

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

Next, when configuring the cell for each individual row, we have to pull out the NSManagedObject and convert it back into our model, so we can use richer properties & behavior that we've defined. This is definitely an uncommon stylistic choice, but we'll run with it to see how it feels.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    EpisodeCell *cell = (EpisodeCell *)[tableView dequeueReusableCellWithIdentifier:@"episodeCell"];
    NSManagedObject *mob = [self.fetchedResultsController objectAtIndexPath:indexPath];
    Episode *episode = [MTLManagedObjectAdapter modelOfClass:[Episode class]
                                           fromManagedObject:mob
                                                       error:nil];

    cell.titleLabel.text = episode.title;
    cell.descriptionLabel.text = episode.episodeDescription;
    cell.thumbnailImageView.imageURL = episode.thumbnailImageUrl;

    return cell;
}

Now if you run the app, you should see your rows on the screen.

Responding to changes

To respond to changes to the underlying core data model, we can use NSFetchedResultsControllerDelegate.


// change the interface declaration to this:
@interface MasterViewController () <NSFetchedResultsControllerDelegate>

// when creating the controller:
_fetchedResultsController.delegate = self;

Next we need to implement the change callbacks:

#pragma mark - NSFetchedResultsControllerDelegate

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

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

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

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

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

        case NSFetchedResultsChangeMove:
            [self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
            break;

        default:
            break;
    }
}

Fix for a row that changes order and needs an update

In the video I had a bug where the row title changed and the published at changed, causing the sort order to change. The row doesn't update until you scroll away and then back again, basically forcing tableView:cellForRowAtIndexPath: to be called again. One way to get around this is to do the keep track of the moved rows and then manually update them after the batch update has finished. The source code reflects this.

It does seem to interfere with the move animation, so if you have a better solution to this problem, please leave a comment (or a pull request).

This episode uses Mantle 1.4.1.