Search code examples
objective-ctableviewnsfetchedresultscontrollernsmanagedobject

TableView sections with NSManaged Object Context Throws Exception


I recently migrated a project from a self-managed object using a config singleton to use the NSManaged Object Context with NSFetchedResultController. What I'm trying to do is fill a TableView with sections that are based on month however the user can select a cell and change the month. when that happens it causes the following exception thrown and the cells become unable to change or edit

    [error] error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  attempt to insert row 0 into section 1, but there are only 0 sections after the update with userInfo (null)
CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  attempt to insert row 0 into section 1, but there are only 0 sections after the update with userInfo (null)

Here is the main view controller fetch request:

- (NSFetchedResultsController<Budget *> *)fetchedResultsController
{
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }
    LogDebug(@"STARTED");
    NSFetchRequest<Budget *> *fetchRequest = Budget.fetchRequest;
    [fetchRequest setFetchBatchSize:20];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"startTime" ascending:NO];
    [fetchRequest setSortDescriptors:@[sortDescriptor]];
    NSFetchedResultsController<Budget *> *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"monthSection" cacheName:nil];
    aFetchedResultsController.delegate = self;

    NSError *error = nil;
    if (![aFetchedResultsController performFetch:&error]) {
        LogError(@"Unresolved error %@, %@", error, error.userInfo);
        abort();
    }

    _fetchedResultsController = aFetchedResultsController;
    return _fetchedResultsController;
}

Then for the Tableview Sections Data Source:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
    NSInteger count = [sectionInfo numberOfObjects];
    LogDebug(@"Number of Rows: %ld in Section %ld",(long)count, (long)section);
    return [sectionInfo numberOfObjects];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    NSInteger count = [[self.fetchedResultsController sections] count];
    LogDebug(@"Sections: %ld",(long)count);
    return [[self.fetchedResultsController sections] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    LogDebug(@"STARTED");
    id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController.sections objectAtIndex:section];
    return [sectionInfo name];
}

I have also tried subclassing the data object model and added this to setting the month section:

- (NSString *)monthSection {

    @synchronized (self.startTime) {
        NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
        [formatter setDateFormat:@"MMMM"];
        NSString *sectionTitle = [formatter stringFromDate:self.startTime];

        return sectionTitle;
    }

}

Now when the user selects a table view cell I send the Budget Object to the DetailViewController by sending it the following way:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        Budget *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
        DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController];
        [controller setDetailItem:object];
        controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
        controller.navigationItem.leftItemsSupplementBackButton = YES;
    }
}

Then in DetailViewController i just use the setter on the startTime:

[startDatePicker setDate:detailItem.startTime];
[[AppDelegate instance] saveContext];
[self totalUpFields];

But once the user changes the Month date to something other than what was initially created it thrown that above exception. I'm very new to the NSManaged Object structure and I've always used a managed config singleton.

For the changed object content here are the methods:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}
- (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;

        default:
            return;
    }
}

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

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

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withBudget:anObject];
            break;

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

Thanks for the help, let me know if i need to add additional details.


Solution

  • I believe this is an issue when moving the last row out of a section. You can resolve it by changing your case NSFetchedResultsChangeMove: to the following:

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

    It looks like this happens because the sections are modified before the rows, so the index paths shift and the expected source/destination may no longer be valid.

    It's also important to first sort by your sectionNameKeyPath:

    NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"monthSection" ascending:NO];
    NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"startTime" ascending:NO];
    [fetchRequest setSortDescriptors:@[sortDescriptor1, sortDescriptor2]];
    

    You will need to add monthSection as a property on your Core Data model and set it when inserting/updating, instead of the dynamic method you're currently using in your subclass (otherwise you'll get an exception when performing the fetch).