NSFetchedResultsController with UITableView

Last time I looked purely at NSFetchedResultsController in isolation, without worrying about the UI it might be driving. Now it’s time to cast an eye over the most typical use case: powering a table view.

My simple test case (same as last time) has a UITableViewController subclass meditating between a table view and a fetched results controller. The various NSFetchedResultsControllerDelegate methods are implemented using Apple’s standard boilerplate like so:

Again, I started off with the simple case of a constant set of objects whose ordering changes pseudo-randomly. There are no sections of fetch predicate.

After each round of updates, the table’s visible cells are checked to make sure they match up with the controller’s objects.

Disappointingly, the test fails pretty quickly. After a slightly complex set of changes, the fetched results controller has correctly sorted its objects, but the resulting changes to the tableview have left it out of sync.

Sections

Next I experimented with ramping up the test a bit by splitting the table into sections. It takes longer, but still fails. The failure can be in the same way as above, with the table getting out of sync with the underlying controller. But weirdly you can also see this code blow up:

The controller throws an exception because there is no object at indexPath. Yes, even though that’s the index path it’s just handed us!

But let’s take a different tack. I expect most of us have seen a message along these lines at some point:

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2380.17/UITableView.m:1070
Invalid update: invalid number of rows in section 2.
The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).

Michael Fey wrote up his troubles with UITableView and NSFetchedResultsController a while back. He was running into the above message, apparently because deleting or inserting sections would effectively report those changes to the table twice, horribly confusing it.I

So I disabled my check of cell values, and instead just fired updates at the table over and over. The good news here is that regardless of what model modifications I threw at it (insert/deleting objects, fetch predicate), the table never once complained its row or sections were invalid.

Michael’s post was written back in early 2013, in the iOS 6 days. So as far as I can see, this issue has been resolved in either iOS 7 or 7.1. I figure either NSFetchedResultsController has gotten better at reporting section changes, or UITableView is more tolerant (I’m too lazy to dig in and find out!).

Moving Rows

So, we know table updates are a bit buggy, with the table sometimes being left out of sync with the controller. It looks to me like this tends to happen only as a consequence of a row being reported as updated, rather than moved. And only when it’s reported that other rows have changed too.

I wondered if the issue might be how moves are handled. Following Apple’s template code, a move is treated as two changes: a deletion of the old row, followed by insertion of the new one.

Since iOS 5, UITableView has offered the ‑moveRowAtIndexPath:toIndexPath: API for directly moving rows. As an experiment you can turn on MOVE_ROWS in the test project, which switches over to the new method. Turns out you really don’t want to do that; the test now fails a lot quicker!

Conclusion

  • Inserting and deleting rows/sections works a treat these days without needing a workaround
  • Moving/updating rows is a bit ropey. I don’t know yet a decent workaround for this; more to come in the next post I hope
  • Do NOT be tempted to use ‑moveRowAtIndexPath:toIndexPath: to handle moved rows. It’s a quick way to come a cropper
© Mike Abdullah 2007-2015