NSFetchedResultsController and AutoLayout

Yesterday I noticed the iPhone version of Stampnote was taking a very long time to save new entries. I would write, tap Done, and then 1-2 seconds would pass before the save completed and I was returned to the previous list of entries.

I could see from my NSLog statements that the save: call was taking an unusually long amount of time to complete. I tweaked my parent-child NSManagedObjectContext setup, eventually reverting to a more traditional two-context setup that relied on calling mergeChangesFromContextDidSaveNotification when one context saved.

This didn't get me anywhere.

I opened Instruments and the Time Profiler and eventually realized the NSFetchedResultsController delegate methods were where the application was spending a lot of time. Digging deeper, I could see a lot of activity centered around AutoLayout calls. When the data model changed, the table views (which aren't visible on screen) would reload some cells.

This app uses the master-detail layout, each with a table view and a fetched results controller. On the iPad, the master list and detail list are visible behind the composer as you write. Both reload quickly when you save, cells are updated appropriately.

On the iPhone, things behave differently. The tables exist, but they're not visible. You only see the current view, where you're composing a new entry. For some reason, the auto layout performance is terrible when table views aren't visible.

I started looking for ways to disable animations (ie, [UIView setAnimationsEnabled:NO]) or defer, in effect, the NSFRC callbacks. I came across a Stack Overflow post that made me more certain I wasn't the only one seeing this; it also gave me something to try.

In the master view controller's NSFRC delegate methods I check to see if the view is installed in the window and, if not, I set a flag that indicates a table reload will be neeeded in viewWillAppear. I left the detail view controller's table alone as it's more complex and I don't really like doing this to begin with.


if (self.tableView.window == nil) {
    self.needsReload = YES;
    return;
}

It's a fairly hokey solution but it sped things up on iPhone considerably. It allows you to defer data model changes that the fetched results controller would normally respond to.

Another solution, if auto layout isn't critical to you, is to turn AutoLayout off on your cell xibs.