I have 2 managed object contexts: (1) created as NSMainQueueConcurrencyType
that is used by the UI/main thread and (2) created as NSPrivateQueueConcurrenc
NSFetchedResultsController
gets terribly confused if you use an NSFetchRequest
when preparing your cell in response to tableView:cellForRowAtIndexPath:
. If you don't execute a NSFetchRequest
, all is well. However, if you do, it triggers NSFetchedResultsController
to perform further change notifications, which does bad things to UITableView
.
The workaround for this is to set includesPendingChanges = NO
on your NSFetchRequest.
I have opened a radar issue about this -- problem id 14048101
-- with a detailed example and sample app. This bug reproduces on iOS 5.1, 6.0, and 6.1.
In my sample app, I added logging to Xcode's CoreData template to log enter/leave of NSFetchedResultsController
delegate methods. When I insert + delete objects on the network context, the logging shows:
01: => (before) mergeChangesFromContextDidSaveNotification 02: => (enter) controllerWillChangeContent count=4 03: <= (leave) controllerWillChangeContent count=4 04: didChangeObject type=1 indexPath=(null) newIndexPath= 2 indexes [0, 0] 05: => (enter) controllerDidChangeContent count=5
At this point, all is good. controllerDidChangeContent:
has been called to process the 1 insert, which calls [tableView endUpdates]
, which calls tableView:cellForRowAtIndexPath:
, which calls configureCell:atIndexPath:
.
06: => (enter) configure cell at row 0
At this point, configureCell:atIndexPath:
creates an NSFetchRequest
and calls [self.managedObjectContext executeFetchRequest:error:]
-- here begins the badness. Executing this fetch request triggers the processing of the remaining changes in the context (1 delete and 3 updates) before processing of the insert has finished (we entered controllerDidChangeContent:
on line #05 and don't leave until line #16).
07: => (enter) controllerWillChangeContent count=5 08: <= (leave) controllerWillChangeContent count=5 09: didChangeObject type=2 indexPath= 2 indexes [0, 4] newIndexPath=(null) 10: didChangeObject type=4 indexPath= 2 indexes [0, 2] newIndexPath=(null) 11: didChangeObject type=4 indexPath= 2 indexes [0, 1] newIndexPath=(null) 12: didChangeObject type=4 indexPath= 2 indexes [0, 3] newIndexPath=(null) 13: => (enter) controllerDidChangeContent count=4
At this point, the framework is making a re-entrant call tocontrollerDidChangeContent:
.
14: <= (leave) controllerDidChangeContent count=4 15: <= (leave) configure cell at row 0 16: <= (leave) controllerDidChangeContent count=4 17: <= (after) mergeChangesFromContextDidSaveNotification
At this point, you can see in the UI that: (1) a new cell has been added, (2) 3 cells were updated, and (3) the deleted cell is still visible, which is wrong.
After further action in the UI, I typically get an Assertion failure
or message sent to an invalid object exception.
My sample app is available at https://github.com/peymano/CoreDataFetchedResultsController