How to implement delayed/batched table view update from NSFetchedResultsControllerDelegate?
The documentation says:
You should consider carefully whether you want to update the table view as each change is made. If a large number of modifications are made simultaneously—for example, if you are reading data from a background thread— /.../ you could just implement controllerDidChangeContent: (which is sent to the delegate when all pending changes have been processed) to reload the table view.
This is exactly what I'm doing: I'm processing incoming changes in a background thread with a different ManagedObjectContext, and merge the results into the main thread MOC with mergeChangesFromContextDidSaveNotification:. So far so good.
I chose to not implement controller:didChangeObject:... and would instead like to do the batched update that the document suggests.
Question/problem: the document doesn't elaborate how to actually implement the batc开发者_运维问答hed update? Should I just call [tableview reloadData] in controllerDidChangeContent: or is there a less intrusive way that saves me from a full reload?
One thought I have: I could take note of mergeChangesFrom... notification that contains the changed objects, figure out their indexpaths, and just call tableview:ReloadRowsAtIndexPaths: for them. But is there any authoritative info, recommendations or examples? Or just [tableview reloadData]?
(Aside: controller:didChangeObject:... started behaving really erratically when it received a set of batched updates, even though the same updating code [that I now put in background thread] was fine before when it was running on the main thread, but of course locking up the UI.)
I would just call reloadData
in controllerDidChangeContent:
.
For animating individual changes to the table, Apple's boiler plate code (iOS SDK 4.3.x) looks like this:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
I believe this is what you're looking for:
Instead of calling
deleteSections
insertSections
reloadSections
deleteRowsAtIndexPaths
insertRowsAtIndexPaths
reloadRowsAtIndexPaths
as the changes are coming in at controller:didChangeObject:atIndexPath:forChangeType:newIndexPath
, collect the indexPaths and sections in properties. Then execute the batch in controllerDidChangeContent:
.
Ref: http://www.fruitstandsoftware.com/blog/2013/02/19/uitableview-and-nsfetchedresultscontroller-updates-done-right/
Edit
Here is a slightly different, more contrived, but also more compact, version of the approach mentioned in the link:
@property (nonatomic, strong) NSMutableArray *sectionChanges;
@property (nonatomic, strong) NSMutableArray *objectChanges;
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
NSMutableDictionary *sectionChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
sectionChange[@(type)] = @(sectionIndex);
break;
}
[self.sectionChanges addObject:sectionChange];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
NSMutableDictionary *objectChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
objectChange[@(type)] = newIndexPath;
break;
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
objectChange[@(type)] = indexPath;
break;
case NSFetchedResultsChangeMove:
objectChange[@(type)] = @[indexPath, newIndexPath];
break;
}
[self.objectChanges addObject:objectChange];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = self.tableView;
NSUInteger totalChanges = self.sectionChanges.count + self.objectChanges.count;
if (totalChanges > 0)
{
[tableView beginUpdates];
if (self.sectionChanges.count > 0)
{
for (NSDictionary *change in self.sectionChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}];
}
}
else if (self.objectChanges > 0)
{
NSMutableArray *indexPathsForUpdatedObjects = [NSMutableArray new];
for (NSDictionary *change in self.objectChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:@[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:@[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[indexPathsForUpdatedObjects addObject:obj];
//[tableView reloadRowsAtIndexPaths:@[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:obj[0] toIndexPath:obj[1]];
break;
}
}];
}
[tableView endUpdates];
for (NSIndexPath *indexPath in indexPathsForUpdatedObjects)
if ([tableView.indexPathsForVisibleRows containsObject:indexPath])
[self updateCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
[self.sectionChanges removeAllObjects];
[self.objectChanges removeAllObjects];
}
else if (totalChanges < 1)
{
[tableView reloadData];
}
}
- (NSMutableArray *)sectionChanges
{
if (!_sectionChanges)
_sectionChanges = [NSMutableArray new];
return _sectionChanges;
}
- (NSMutableArray *)objectChanges
{
if (!_objectChanges)
_objectChanges = [NSMutableArray new];
return _objectChanges;
}
Note that this solution is not perfect in that it does not update objects if there are changes to sections and those objects are in a section that is not updated. Should be easy to fix (and is irrelevant for many applications).
You could note the last index path in your datasource & then do this -
NSIndexPath *indexPath = nil;
NSMutableArray *newResults = [[NSMutableArray alloc] init];
if(gLastResultIndex < [self.dataSource count])
{
for(int i=(gLastResultIndex); i<[self.dataSource count]; i++)
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[newResults addObject:indexPath];
}
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:newResults withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates];
}
[newResults release];
This is selectively adding new rows to your existing UITableView
. reload table reloads all cells in your table which you might not need. I use reload table only when the entire datasource changes...
精彩评论