-
-
Save MrRooni/4988922 to your computer and use it in GitHub Desktop.
@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 |
I see you're calling [self.tableView beginUpdates] inside controllerDidChangeContent - does that mean you've chosen not to implement controllerWillChangeContent ?
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.
Also worth noting that if totalChanges > 50 the collections are never nilled out and so animation will never be used again
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.
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.
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.