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

Solution:

If you have this assertion you probably use reloadRowsAtIndexPaths:withRowAnimation: on FRC NSFetchedResultsChangeUpdate

The idea to fix this is pretty simple. Maybe you noticed that all NSFetchedResultsChangeUpdate also have corresponding NSFetchedResultsChangeMove change type with equal newIndexPath and indexPath. So, move your code from NSFetchedResultsChangeUpdate to NSFetchedResultsChangeMove if you use iOS9 SDK and everything should be fine

Take a look at the code below:

- (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:

// Do nothing here

break;

case NSFetchedResultsChangeMove:

if ([indexPath compare:newIndexPath] == NSOrderedSame) {

// Just to clarify, if you have indexPath = newIndexPath reloadRowsAtIndexPaths

// Simple moveRowAtIndexPath should also work

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

}

else {

[self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];

}

break;

}

}

You can chose to not relay on the buggy FRC for your tableview to update the UI. Alternatively you can implement frc notification yourself like this.


https://gist.github.com/MrRooni/4988922

Oh wow, Xcode 7.0.1 now also sends .Move notifications with indexPath == newIndexPath to iOS 9 devices on object updates...

I have this exact problem, no error in the console, the delegate of the FRC is called correctly, just that sometimes after inserting new lines in my table view, they appears as blank


yes, it's was an iOS 8.4.1 app started with Xcode 6, I changed the deployment target for iOS 9 and still got this issue


it looks like a refresh problem, however I can't go to iOS 9 without resolving this issue,


I will have to create a bug report to apple


thanks

How come this big issue is still on with the version 7.2?!

Does anyone actually have a solution to this? None of the solutions people have posted work for me at all.

I can't used Xcode 7 because my company has to support iOS 8, but I can't use Xcode 6 becaues El Capitan won't build the storyboards.

Xcode 7.2 is out, how can Apple still be ignoring this?

You can try the dance I have been using: https://github.com/JohnEstropia/CoreStore/blob/master/CoreStore/Internal/FetchedResultsControllerDelegate.swift


It works for the most part, and handles the edge cases we've seen so far.


According to this open radar https://openradar.appspot.com/22684413 ,

Apple's reply was that it "behaves as intended" as a non-backportable iOS 9 fix... (iOS 9 has it's own FRC problems though)

If you read the comment in the header file for NSFetchedResultsControllerDelegate you'll see it is behaving as expected:


The Move object is reported when the changed attribute on the object is one of the sort descriptors used in the fetch request. An update of the object is assumed in this case, but no separate update message is sent to the delegate.

The Update object is reported when an object's state changes, and the changed attributes aren't part of the sort keys.


With all of this information to hand, the solution is very simple. If there has been a move, move and reload. If there was no real move (due to the update being to a property involved in the sort key) them simply reload!


        case NSFetchedResultsChangeMove:
        {
            if(![indexPath isEqual:newIndexPath]){
                [tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [tableView reloadRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
                });
            }else{
                [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            }
            break;
        }


Note: the reason for the dispatch async is the table view doesn't accept two animations in the same run loop event so I had to split them up.

It's been a few months but iOS 10 still have this problem so I'll continue the discussion.


The fact that you have to do a dispatch_async is exactly the problem. NSFRC was meant to work with UITableView and UICollectionView and the events sent by NSFRCDelegate should work with the tableView's beginUpdate-endUpdate() transaction as is.