iosdev

One (not)weird trick to save your sanity with NSFetchedResultsController

I have no idea how many weeks I have wasted debugging issues that come down to this problem. I am pretty mad at myself for forgetting this, but oh boy, it’s not gonna happen anymore. Oh no.

Ok, so you have NSFetchedResultsController driving either a collection view or table view. And when customer is looking at those views, you want the changes to animate in/out, as Apple has ask us to do since the days when iOS was called iPhone OS. To do that, you need to implement the four horsemen of NSFetchedResultsControllerDelegate methods.

However - and this is the trick - you don’t need to do that when those views are not visible. You only need to call reloadData on end of changes. However, I guarantee you that 95% of iOS devs leave those four methods as they are. And experience hair-pulling mind-cracking EXC_BAD_ACCESS crashes all over the place, in darn background threads that cause postNotification:..:... and what not. And you have questions about this on StackOverflow being answered with

oh, just set self.fetchedResultsController.delegate to nil in viewWillDisappear and problem goes away. Re-set it on viewWillAppear

You don’t say! Well, it sure goes away, but you also lose all the changes and your views do not reflect the current state of data source. And then when you try to animate, more crashes ensue…

oh, that’s easy to fix. Just add [self.collectionView reloadData] in viewWillAppear

AAARRGGHHH!

No. Do not do any of that crap.

For the love of your sanity and the trust of your customers, make sure you always have this in your code:

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
	if (self.collectionView.window == nil)
		return;

	...	
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
	if (self.collectionView.window == nil)
		return;

	...	
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

	if (self.collectionView.window == nil) {
		[self.collectionView reloadData];
		return;	
	}
	
	...
}

and the equivalent for table view.

Update (Jul 25th, 2014): Kyle Fuller correctly points out on Twitter that this above is subject to race condition issue: that .window might become (or not) nil in-between these various delegate calls. It’s better to collect the changes as they happen and then when you get to controllerDidChangeContent: decide what to do, based on .window value. Which is exactly what I do in the UICV category mentioned below, but overlooked that when writing the blog post.

To repeat: if your views are not in the currently visible window, then simply do reloadData. That is all. Class dismissed.

The code above is just a dummy thing. For my part, I have a custom category for NSFetchedResultsController and UICollectionView that does this automatically without repeated checks for window in each method, but the essence is what I wrote above.

You can find the UICollectionView category at GitHub.