iOS 9 CoreData NSFetchedResultsController update causes blank rows in UICollectionView/UITableView

Description:

I have separate classes that run NSFetchedResultsController on both UITableView and UICollectionView. They've been working in iOS 8. Since iOS 9, I occasionally get the console error below on NSManagedObject updates. The changes to NSManagedObject is valid and saves properly to persistent store. However, the UI for UITableView/UICollectionView backed by the NSFetchedResultsController breaks and shows blank rows.


Console:

Assertion failure in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:]

CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete and reload the same index path (<NSIndexPath: 0xc000000000400016> {length = 2, path = 0 - 2}) with userInfo (null)


Update:

iOS9 Beta 2 not fixed and no solution yet, but below's the line that's causing the trouble:


self.tableView.reloadRowsAtIndexPaths()


Seems like it's having trouble reloading cells on updates.


Update 2:

iOS9 Beta 3 still broken.

The root cause of all this is during the NSFetchedResultsController's delegate call for an update, NSFetchedResultsChangeMove and NSFetchedResultsChangeUpdate are both called. Not sure if this use to happen in iOS 8 and prior but just didn't throw off errors.


Update 3:

Issue fixed once updated to iOS 9 Beta 5

Replies

I'm facing a similar issue in the table view:


*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1406
CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  attempt to insert row 1 into section 0, but there are only 1 rows in section 0 after the update with userInfo (null)


Although for me it is happening only when deploying on a device (iOS 8.3). It is working in the simulator.


After a restart of the app the cells are displayed correctly.

Ya. CoreData actually works fine, things get updated. Thus, restarting the app things go back to normal. It seems to be updates that are causing this issue.


I stepped through the debugger, believe it's a iOS 9 bug at the following line:


self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: self.rowAnimation)


Reloading rows causes the error. Not sure what we can do here except wait for iOS 9 to fix this.

I am also seeing this error on the simulator in iOS 9. My project was working correctly on iOS 8 and now this error is appearing on my table views after a save.


Here is my NSFetchedResultsControllerDelegate code:


# pragma mark - NSFetchedResultsControllerDelegate
- (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:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
        default:
            break;
    }
}
- (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 NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        default:
            break;
    }
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}


And here is the error:


CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete and reload the same index path (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}) with userInfo (null)

Hi!


I get the same thing. Was hoping that it would be solved in Beta 2, but the issue persist.


Did you report this to Apple in a bug report?


/ Stefan

I am having the same issue. No problems in iOS 8, same exact crash from same code in iOS 9. Wish I could be of more help, but want to confirm that I am also having this issue.

I think this may have something to do with a fix many of us use in the NSFetchedResultsController Delegate methods, specically


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath


where we replace


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


with


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


When I change an object and both NSFetchedResultsChangeUpdate and NSFetchedResultsChangeMove are called, I get the error you speak of. When I comment out

[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];


that error goes away. Not sure what the final solution is, since the fix seems to be a needed one (I had included a link to an article by Ole Begemann on why this was needed but I think Apple needs to moderate those comments, meaning not showing them for a certain amount of time, and I think it is more important to post this comment than to include the link, which can be Googled).

Just to report here I'm getting the same error in iOS 9 (both beta 1 and 2):


*** Assertion failure in -[YCTableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3480.5/UITableView.m:1362
attempt to perform a delete and a move from the same index path (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0})

Thanks!


That was it. I do not even remember that I did that change. I might even have copied the whole CoreDataTableViewContoller file from a course I did. Anyway, changing back to what is described in the documentation solved my issue.

No I haven't. I have trouble knowing what component to file bugs to 😟

The Problem is in NSFetchedResultsChangeMove handling. If indexPath with newIndexPath are equal, then the error occures.


Fix:


case NSFetchedResultsChangeMove:
     if (![indexPath isEqual:newIndexPath]) {
          [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
          [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
     }
     break;

You're answer is partially correct and helps with the case where the indexPaths are equal. This would be an update where the item is not required to be moved/re-ordered. However, I'm still running into the same problem if the new update is reordered.


Use case:

A table with sort order by name: A, B, C

If we change item A to become D, the new table becomes B, C, D and the error still occurs.


The root cause of all this is during the NSFetchedResultsController's delegate call for an update, NSFetchedResultsChangeMove and NSFetchedResultsChangeUpdate are both called. Not sure if this use to happen in iOS 8 and prior but just didn't throw off errors.

I'm having exactly the same issue with my FRC: previously it would report just an update, now it reports an update, and a move from one index path to the same index path, which is nonsense. I've submitted a radar for this (rdar://21888197). In my case, this issue seems to be related to the change being saved in a separate context, and then merged to the "main" context which FRC uses.

Thanks for filing the radar, hope we get the fix soon. I've also gotten the error operating only in the main context, so I know it's not strictly a threading or context merge issue.

My radar is now in state "Duplicate of 17392525 (Closed)" – so hopefully we'll see this bug fixed in Seed 5 (Seed 4 still has it).

Same crash as everyone else on beta 4:


2015-07-28 18:08:50.191 MyApp[85254:24219439] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to perform a delete and a move from the same index path (<NSIndexPath: 0xc000000000400016> {length = 2, path = 0 - 2}) with userInfo (null)

I sure hope the fix it as its one of the best classes and would be a real shame to have to re-implement it ourselves.

By the way for a ChangeMove the delete/insert can be replaced with move for a nicer animation:

        case NSFetchedResultsChangeMove:
            if(![indexPath isEqual:newIndexPath]){ // iOS 9 beta fix
                [tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
            }
            break;


It's been available since iOS 5 but for whatever reason Apple haven't included it in their project templates yet.


Furthermore in your ChangeUpdate you should reload the cell and not just configure it. The reason is your data change might require a change in cell prototype. If just configuring you aren't able to do that. Then you can use this:


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSManagedObject* object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    return [self cellForRowAtIndexPath:indexPath withObject:object];
}

- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath withObject:(NSManagedObject*)object{
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:_cellReuseIdentifier forIndexPath:indexPath];
    if(!cell){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_cellReuseIdentifier];
    }
    return cell;
}


And then in your subclass just override the cellForRowAtIndexPath:withObject is enough, and you can call super if you want to get the default cell. I have a property for the default cell reuse identifier so I can change it to match the storyboard if necesary but usually I just leave it as "Cell".


The full class is up on my Github here: https://github.com/malcolmhall/MHData/blob/master/MHData/MHFetchedResultsViewController.m