I thought I got it. But a new crash I found in my app says otherwise. So any one knows the really correct code for NSFetchedResultsChangeUpdate
when newIndexPath is non-nil and not the same as indexPath in -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
?

- 17,191
- 12
- 86
- 136
-
What is the error message/stack backtrace? – Martin R Sep 16 '12 at 06:05
-
newIndexPath is out of bound. – an0 Sep 16 '12 at 16:08
-
3I had a similar problem, and my solution/workaround(?) was not to use indexPath/newIndexPath at all for an update event. You can see my question and self-supplied answer here: http://stackoverflow.com/questions/11432556/nsrangeexception-exception-in-nsfetchedresultschangeupdate-event-of-nsfetchedres. Please let me know if it helps! – Martin R Sep 16 '12 at 16:19
-
@MartinR thanks, it seems a feasible workaround, though I'm still wondering what is the canonical way for us to use `indexPath` and `newIndexPath` for `NSFetchedResultsChangeUpdate`. – an0 Sep 17 '12 at 22:08
-
Yes, same for me. All sample code that you see uses only `indexPath` for the update event. My question at SO got no feedback at all, and a similar question at the Apple Developer Forum got almost no feedback. So we seem to be the only two people with this problem (-: – Martin R Sep 18 '12 at 05:17
-
1Ole Begemann has a blog entry discussing this issue. He says it's a bug in the docs, and when he filed a Radar it was closed as a duplicate. http://oleb.net/blog/2013/02/nsfetchedresultscontroller-documentation-bug/ – Kristopher Johnson Jan 04 '14 at 14:27
-
@KristopherJohnson the correct logic is to use `newIndexPath` for data query. See my original article from which this question came from: http://wangling.me/2011/09/bugs-of-nsfetchedresultscontrollerdelegate-template-code.html – an0 Jan 04 '14 at 15:14
-
@an0 I don't like Begemann's solution either. I prefer MartinR's solution at http://stackoverflow.com/questions/11432556/nsrangeexception-exception-in-nsfetchedresultschangeupdate-event-of-nsfetchedres. Better to update the existing cell in place rather than dequeue a new one, initialize it, and then reload the row or table. – Kristopher Johnson Jan 04 '14 at 17:20
-
@KristopherJohnson Me too. Read the discussion between Martin and me above. – an0 Jan 05 '14 at 15:58
2 Answers
I just ran into a crash on update, and it appears that the newIndexPath
is provided as an index path into the fetched results controller objects when that index path does not match the index path needed to retrieve the cell from the table. Consider the following:
- A table view has a section at index 0 with 15 items.
- The item at index 13 (the second-to-last item) is deleted
- The item at index 14 (the last item) is updated
In the above case, assuming that you are using [tableView beginUpdates]
and [tableView endUpdates]
in the appropriate controllerWill/DidChangeContent:
methods, you will need to use the indexPath
parameter to retrieve the cell from the table to update (which will be section 0, index 14) and the newIndexPath
parameter to retrieve the object to configure the cell with from the results controller (which will be section 0, index 13).
I assume it works this way because the delete seems to have already happened as far as the results controller is concerned, but has not happened in the table view (due to the beginUpdates/endUpdates
calls wrapping the updates). It makes some sense if you consider the above case, but it seems that all of the documentation does not consider this case.
So the answer to the question is that it appears that you should use the indexPath
parameter to retrieve a cell from the table view, and the newIndexPath
parameter to retrieve the object from the fetched results controller. Note that if there has not been an insert or delete it appears to pass nil
for newIndexPath
, so in this case you would have to use indexPath
for both purposes.

- 10,685
- 1
- 42
- 39
-
Upon further reflection, it seems to me that the NSFetchedController should be calling the delegate method with a move event instead of an update event. It does this in the simple test case I just created. It's a mystery to me at the moment what condition triggers an update instead. – Charles A. Aug 08 '13 at 22:39
-
If you read the article I linked to in the question you should see that it is exactly what I did. – an0 Aug 09 '13 at 14:32
-
My mistake, I didn't actually see the link until you pointed it out. What line of code is causing the crash in your case? Which NSIndexPath are you using and what are you doing with it? – Charles A. Aug 11 '13 at 17:07
-
I had some discussion with @Martin R in the comments of the question post. You can check them out. – an0 Aug 12 '13 at 15:12
-
@CharlesA. I changed the order of index 13 with index 14, otherwise it didn't look sensible. Can you confirm that I am doing the right thing? – Özgür Jul 19 '14 at 00:28
-
@Comptrol Is there a way I can compare the current edit to the previous revision? I wrote this a year ago, so I don't recall exactly what it said. – Charles A. Jul 27 '14 at 20:27
-
@CharlesA. I just changed 13 to 14 and 14 to 13 in the paragraph beginning with "In the above case..." – Özgür Jul 29 '14 at 07:35
-
1@Comptrol It was actually correct the way it was (I changed it back). In the case I'm talking about the object has been deleted from the fetched results controller but not the table view. So the table view has a related cell at index 14 (because the deleted object's cell is still present) and the fetched results controller has the object at index 13 (because the deleted object that was at index 13 is gone). – Charles A. Jul 31 '14 at 05:53
-
-
If an object of NSFetchedResultsController changes and moves at the "same time" -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
sends type NSFetchedResultsChangeUpdate
(see here).
My solution is to change the type to move everytime the type is update and indexPath is not equal to newIndexPath
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView;
if (controller == self.fetchedResultsController) {
tableView = self.tableView;
}
else {
tableView = self.searchDisplayController.searchResultsTableView;
}
// type is "update" ---> should be "move"
if (type == NSFetchedResultsChangeUpdate && [indexPath compare:newIndexPath] != NSOrderedSame && newIndexPath != nil) {
type = NSFetchedResultsChangeMove;
}
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self fetchedResultsController:controller configureCell:(UITableViewCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationRight];
break;
}
}
Afterwards you have to update the table view
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
[self.tableView reloadData];
}
I hope this is helpful!

- 3,329
- 27
- 28
-
2
-
In that case where change type is "update" but should be "move" the table view doesn't update correctly. Therefore you have to reload the table view. Maybe it isn't necessary in iOS8 and above, it was in iOS7. – Hans One Oct 07 '15 at 12:47
-
I've been looking for this answer for years. It works perfect. Thank you – elemento Dec 09 '16 at 06:11
-
The `reloadData` defeats the purpose of observing the changes. If you do not care about nice animations you can simply call `relaodData` in `controllerDidChangeContent` ands no other code is needed. – Stefan Vasiljevic Aug 28 '18 at 17:36