Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
UITableView and NSFetchedResultsController: Updates Done Right
@interface SomeViewController ()
// Declare some collection properties to hold the various updates we might get from the NSFetchedResultsControllerDelegate
@property (nonatomic, strong) NSMutableIndexSet *deletedSectionIndexes;
@property (nonatomic, strong) NSMutableIndexSet *insertedSectionIndexes;
@property (nonatomic, strong) NSMutableArray *deletedRowIndexPaths;
@property (nonatomic, strong) NSMutableArray *insertedRowIndexPaths;
@property (nonatomic, strong) NSMutableArray *updatedRowIndexPaths;
@end
@implementation SomeViewController
#pragma mark - NSFetchedResultsControllerDelegate methods
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
if (type == NSFetchedResultsChangeInsert) {
if ([self.insertedSectionIndexes containsIndex:newIndexPath.section]) {
// If we've already been told that we're adding a section for this inserted row we skip it since it will handled by the section insertion.
return;
}
[self.insertedRowIndexPaths addObject:newIndexPath];
} else if (type == NSFetchedResultsChangeDelete) {
if ([self.deletedSectionIndexes containsIndex:indexPath.section]) {
// If we've already been told that we're deleting a section for this deleted row we skip it since it will handled by the section deletion.
return;
}
[self.deletedRowIndexPaths addObject:indexPath];
} else if (type == NSFetchedResultsChangeMove) {
if ([self.insertedSectionIndexes containsIndex:newIndexPath.section] == NO) {
[self.insertedRowIndexPaths addObject:newIndexPath];
}
if ([self.deletedSectionIndexes containsIndex:indexPath.section] == NO) {
[self.deletedRowIndexPaths addObject:indexPath];
}
} else if (type == NSFetchedResultsChangeUpdate) {
[self.updatedRowIndexPaths addObject:indexPath];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.insertedSectionIndexes addIndex:sectionIndex];
break;
case NSFetchedResultsChangeDelete:
[self.deletedSectionIndexes addIndex:sectionIndex];
break;
default:
; // Shouldn't have a default
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSInteger totalChanges = [self.deletedSectionIndexes count] +
[self.insertedSectionIndexes count] +
[self.deletedRowIndexPaths count] +
[self.insertedRowIndexPaths count] +
[self.updatedRowIndexPaths count];
if (totalChanges > 50) {
[self.tableView reloadData];
return;
}
[self.tableView beginUpdates];
[self.tableView deleteSections:self.deletedSectionIndexes withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertSections:self.insertedSectionIndexes withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:self.deletedRowIndexPaths withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView insertRowsAtIndexPaths:self.insertedRowIndexPaths withRowAnimation:UITableViewRowAnimationRight];
[self.tableView reloadRowsAtIndexPaths:self.updatedRowIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
// nil out the collections so their ready for their next use.
self.insertedSectionIndexes = nil;
self.deletedSectionIndexes = nil;
self.deletedRowIndexPaths = nil;
self.insertedRowIndexPaths = nil;
self.updatedRowIndexPaths = nil;
}
#pragma mark - Overridden getters
/**
* Lazily instantiate these collections.
*/
- (NSMutableIndexSet *)deletedSectionIndexes
{
if (_deletedSectionIndexes == nil) {
_deletedSectionIndexes = [[NSMutableIndexSet alloc] init];
}
return _deletedSectionIndexes;
}
- (NSMutableIndexSet *)insertedSectionIndexes
{
if (_insertedSectionIndexes == nil) {
_insertedSectionIndexes = [[NSMutableIndexSet alloc] init];
}
return _insertedSectionIndexes;
}
- (NSMutableArray *)deletedRowIndexPaths
{
if (_deletedRowIndexPaths == nil) {
_deletedRowIndexPaths = [[NSMutableArray alloc] init];
}
return _deletedRowIndexPaths;
}
- (NSMutableArray *)insertedRowIndexPaths
{
if (_insertedRowIndexPaths == nil) {
_insertedRowIndexPaths = [[NSMutableArray alloc] init];
}
return _insertedRowIndexPaths;
}
- (NSMutableArray *)updatedRowIndexPaths
{
if (_updatedRowIndexPaths == nil) {
_updatedRowIndexPaths = [[NSMutableArray alloc] init];
}
return _updatedRowIndexPaths;
}
@end

Is there a reason that when the total changes is greater than 50 you just reload the table but do not nil out the collections again? I would have assumed you would have to nil them out at this point also.

I see you're calling [self.tableView beginUpdates] inside controllerDidChangeContent - does that mean you've chosen not to implement controllerWillChangeContent ?

prendio2 commented Oct 8, 2013

Not sure if this is new to iOS 7 but in current testing didChangeObject:atIndexPath: gets called before didChangeSection:atIndexPath: for NSFetchedResultsChangeDelete. Therefore self.deletedSectionIndexes doesn't contain that section yet and we still end up deleting both the object and the section in controllerDidChangeContent:.

This hasn't caused any crashes for me but thought it was worth noting.

prendio2 commented Oct 8, 2013

Also worth noting that if totalChanges > 50 the collections are never nilled out and so animation will never be used again

nobre84 commented Nov 1, 2013

Hi, your solution is very good. Could you attach a license disclaimer to the code above ?

My team found this snippet useful, with one slight modification. If you're having strange crashes, this might help!

It turns out that [tableView beginUpdates] sometimes checks numberOfRowsInSection. Then, endUpdates checks the number again, and it compares its value against the value obtained earlier. If the "before" value was obtained before the update cycle (which is usually what happens), everything is fine. But if beginUpdates checks the "before" value, there are problems.

For example, if you have 100 rows at the beginning and do one insertion, an assertion in endUpdates expects to see 101 rows. It gets very unhappy if it sees 101 at the beginning, and 101 at the end (even though 101 is correct).

Because of this, we found it better to put beginUpdates in controllerWillChangeContent so that it's guaranteed to execute before the data source is updated. That clobbers the totalChanges > 50 trick, but it gets rid of an intermittent crash.

catlan commented May 13, 2014

You can find a fork based on the comments of @prendio2 here: https://gist.github.com/catlan/01eb0f83554e44335c2a

hborders commented Jun 8, 2015

What is the license for this gist?

another fork

fix iOS8/9 coredata context merge issue
UITableView UICollectionView both works

https://gist.github.com/TonnyTao/314fe120ceaf702c0aa9

Owner
MrRooni commented Sep 15, 2016

Hey folks. Wow, sorry that I never saw these messages. This code is free to use for whoever finds it useful. There is no license for it, so go nuts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment