Created
February 19, 2013 19:16
-
-
Save MrRooni/4988922 to your computer and use it in GitHub Desktop.
UITableView and NSFetchedResultsController: Updates Done Right
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
You can find a fork based on the comments of @prendio2 here: https://gist.github.com/catlan/01eb0f83554e44335c2a
What is the license for this gist?
another fork
fix iOS8/9 coredata context merge issue
UITableView UICollectionView both works
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
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 checksnumberOfRowsInSection
. 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 ifbeginUpdates
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
incontrollerWillChangeContent
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.