5

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.

Mark
  • 7,167
  • 4
  • 44
  • 68

1 Answers1

4

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.

Community
  • 1
  • 1
Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
  • The issue with this is the actual array I'm displaying is updated in a separate class, and I send a notification to my tableView class to reload the rows to reflect that. So I can't send the tableView updates to the end of the main queue, because by the time it's executed, the data source will have changed. – Mark Aug 09 '13 at 13:48
  • This shouldn't be an issue. In fact, the solution I linked gets exactly like this. I just simplified the code here because you didn't say you were doing it that way :) The trick is you update the data model and post the notification inside the same block. – Timothy Moose Aug 09 '13 at 15:22
  • In the example you link, the array is modified in the dispatch sync block. My array would be modified before addRow:atIndexPath: is executed. – Mark Aug 09 '13 at 15:25
  • Interesting. Any reason why you can't move the array modification into the block? Maybe if you share more details of the problem, we can come up with a workaround. – Timothy Moose Aug 09 '13 at 15:30
  • Basically I have a manager class which handles connecting to bluetooth accessories. So when it scans for them, they can appear at time. The manager keeps track of the devices in an array, and my view controller references the array and tries to update when it changes. – Mark Aug 09 '13 at 15:34
  • I'm still not seeing the problem. In your manager class, what would break if you updated the array in the block? – Timothy Moose Aug 09 '13 at 15:42
  • The manager class is separate from the viewController class, so it doesn't know about the viewController class to update its tableView. I could try using an updateBlock and execute the update block every time the array changes... I'll give that a shot. – Mark Aug 09 '13 at 15:47
  • Got it. Still sounds exactly like the linked scenario. The manager class doesn't need to know about recipient of the notification. To be clear, the table doesn't need to perform the batch updates in a block anymore. Updating the array and posting the notification in the block is sufficient. – Timothy Moose Aug 09 '13 at 15:54
  • Ah after looking at my code again, I am doing what you recommended in this answer, except I am using dispatch_async rather than dispatch_sync for launching to the main thread. I am sure changing it to sync will fix the problem. Thanks! – Mark Aug 14 '13 at 15:53
  • @TimothyMoose This post says Async with Sync causes deadlock – Could your solution cause deadlock?http://stackoverflow.com/questions/18262066/what-will-happen-if-i-have-nested-dispatch-async-calls/18262699 – k-thorat Jun 22 '16 at 01:49
  • @D-Griffin No, it does not cause a deadlock because dispatch_sync is only used in one direction, from the dispatch queue to the main queue. – Timothy Moose Jun 23 '16 at 15:32
  • @TimothyMoose Got it...Thanks! – k-thorat Jun 29 '16 at 20:18