开发者

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...

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜