Search code examples
iosuitableviewatomic

What's the best way to ensure a UITableView reloads atomically?


I'v got a UITableView whose dataSource updated at random intervals in a very short period of time. As more objects are discovered, they are added to the tableView's data source and I insert the specific indexPath:

[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];

The data source is located in a manager class, and a notification is posted when it changes.

- (void)addObjectToDataSource:(NSObject*)object {
    [self.dataSource addObject:object];
    [[NSNotificationCenter defaultCenter] postNotification:@"dataSourceUpdate" object:nil];
}

The viewController updates the tableView when it receives this notification.

- (void)handleDataSourceUpdate:(NSNotification*)notification {
    NSObject *object = notification.userInfo[@"object"];
    NSIndexPath *indexPath = [self indexPathForObject:object];

    [self.tableView beginUpdates];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.tableView endUpdates];
}

This works fine, but I noticed that in some cases, a second object is discovered just as the first one is calling endUpdates, and I get an exception claiming I have two objects in my data source when the tableView was expecting one.

I was wondering if anyone has figured out a better way to atomically insert rows into a tableView. I was thinking of putting a @synchronized(self.tableView) block around the update, but I'd like to avoid that if possible because it is expensive.


Solution

  • The method I've recommended is to create a private queue for synchronously posting batch updates onto the main queue (where addRow is a method that inserts an item into the data model at a given indexPath):

    @interface MyModelClass ()
    @property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
    @end
    
    @implementation MyModelClass
    
    - (dispatch_queue_t)myDispatchQueue
    {
        if (_myDispatchQueue == nil) {
            _myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
        }
        return _myDispatchQueue;
    }
    
    - (void)addRow:(NSString *)data atIndexPath:(NSIndexPath *)indexPath
    {
        dispatch_async(self.myDispatchQueue, ^{
            dispatch_sync(dispatch_get_main_queue(), ^{
                //update the data model here
                [self.tableView beginUpdates];
                [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
                [self.tableView endUpdates];
            });
        });
    }
    

    By doing it this way, you don't block any other threads and the block-based approach ensures that the table view's animation blocks (the ones that are throwing the exceptions) get executed in the right order. There is a more detailed explanation in Rapid row insertion into UITableView causes NSInternalInconsistencyException.