I previously wrote about how to implement NSFetchedResultsController with Magical Record (UITableView version).
For UICollectionView
, it is largely the same.
Except for the change that Apple did - there is NO [collectionView beginUpdates/endUpdates]
methods.
Therefore, a working implementation for the 4 Crazy NSFetchedResultsControllerDelegate methods is needed to the earlier post.
The code below is exactly from Jose, who adapted the code from AshFurrow. Credits to them.
Firstly, you have to add 2 NSMutableArray
to your view controller to store the changes.
@interface MyViewController ()
@property NSMutableArray *sectionChanges;
@property NSMutableArray *itemChanges;
@end
In the 4 Crazy NSFetchedResultsControllerDelegate methods, you bascially add to the arrays, and finally update them in batches in controllerDidChangeContent
.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
_sectionChanges = [[NSMutableArray alloc] init];
_itemChanges = [[NSMutableArray alloc] init];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type {
NSMutableDictionary *change = [[NSMutableDictionary alloc] init];
change[@(type)] = @(sectionIndex);
[_sectionChanges addObject:change];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
NSMutableDictionary *change = [[NSMutableDictionary alloc] init];
switch(type) {
case NSFetchedResultsChangeInsert:
change[@(type)] = newIndexPath;
break;
case NSFetchedResultsChangeDelete:
change[@(type)] = indexPath;
break;
case NSFetchedResultsChangeUpdate:
change[@(type)] = indexPath;
break;
case NSFetchedResultsChangeMove:
change[@(type)] = @[indexPath, newIndexPath];
break;
}
[_itemChanges addObject:change];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.collectionView performBatchUpdates:^{
for (NSDictionary *change in _sectionChanges) {
[change enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch(type) {
case NSFetchedResultsChangeInsert:
[self.collectionView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]]];
break;
case NSFetchedResultsChangeDelete:
[self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]]];
break;
}
}];
}
for (NSDictionary *change in _itemChanges) {
[change enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch(type) {
case NSFetchedResultsChangeInsert:
[self.collectionView insertItemsAtIndexPaths:@[obj]];
break;
case NSFetchedResultsChangeDelete:
[self.collectionView deleteItemsAtIndexPaths:@[obj]];
break;
case NSFetchedResultsChangeUpdate:
[self.collectionView reloadItemsAtIndexPaths:@[obj]];
break;
case NSFetchedResultsChangeMove:
[self.collectionView moveItemAtIndexPath:obj[0] toIndexPath:obj[1]];
break;
}
}];
}
} completion:^(BOOL finished) {
_sectionChanges = nil;
_itemChanges = nil;
}];
}
Pitfall: fetchLimit
If you use fetchLimit
when you performFetch
, you might be thinking it will be “honoured” throughout the NSFetchedResultsController
eg. if they is change in the data, it will still be limited
Unfortunately, it does not limit the number of records.
A quick solution is to performFetch
again, whenever there is a change in content:
override func controllerDidChangeContent(controller: NSFetchedResultsController) {
_ = try? fetchedResultsController.performFetch()
}
Another solution is to limit the number of cells to display in the UICollectionViewDataSource
methods. (Disclaimer: I didn’t have success with that, as the code produces “CoreData: error: Serious application error… controllerDidChangeContent…”, but the concept is correct)