Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rapid row insertion into UITableView causes NSInternalInconsistencyException

I have a UITableView that sometimes has rapid insertions of new rows. The insertion of the new rows is handled by a notification observer listening for the update notification fired whenever the underlying data changes. I use a @synchronized block around all the data model changes and the actual notification post itself... hoping that each incremental data change (and row insertion) will be handled separately. However, there are times when this still fails. The exception will tell me that it expects 10 rows (based on the count from the data model), it previously had 8 rows, but the update notification only told it to insert a single row (as this is the first of two rapidly fired notifications).

I'm trying to understand how other people tend to handle these types of situations. How do other developers mitigate the problems of having multi-threaded race conditions between two table view update operations? Should I have a more secure lock that controls the update notifications (and why isn't @synchronized doing what it's supposed to)?

Any advice is greatly appreciated! Thanks.

Some pseudo-code:

My model class has a method like this, which gets called by other threads to append new rows to the table view:

- (void)addRow:(NSString *)data
{
    @synchronized(self.arrayOfData)
    {
        NSInteger nextIndex = self.arrayData.count;
        [self.arrayData addObject:data];
        [NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:@{@"insert": @[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
    }
}

My controller class has a method like this to accept the kDataUpdatedNotification notification and actually perform the row insertion:

- (void)onDataUpdatedNotification:(NSNotification *)notification
{
    NSDictionary *changes = notification.userInfo;
    [self.tableView insertRowsAtIndexPaths:changes[@"insert"] withRowAnimation:UITableViewRowAnimationBottom];
} 
like image 473
Mr. T Avatar asked Feb 15 '23 22:02

Mr. T


1 Answers

You're going to have this problem if you change your data model asynchronously with the main queue because your table view delegate methods are looking at the current state of the data model, which may be ahead of the inserts you've reported to the table view.

UPDATE

One solution is to queue your updates on a private queue and have that queue update your data model on the main queue synchronously (I have not tested this code):

@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
{
    dispatch_async(self.myDispatchQueue, ^{
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSInteger nextIndex = self.arrayData.count;
            [self.arrayData addObject:data];
            [NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:@{@"insert": @[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
        });
    });
}

The reason you need the intermediate dispatch queue is as follows. In the original solution (below), you get a series of blocks on the main queue that look something like this:

  1. Add row N
  2. Add row N+1
  3. Block posted by table view for row N animation
  4. Block posted by table view for row N+1 animation

In step (3), the animation block is out-of-sync with the table view because (2) happened first, which results in an exception (assertion failure, I think). So, by posting the add row blocks to the main queue synchronously from a private dispatch queue, you get something like the following:

  1. Add row N
  2. Block posted by table view for row N animation
  3. Add row N+1
  4. Block posted by table view for row N+1 animation

without holding up your worker queues.

ORIGINAL Solution still has issues with overlapping animations.

I think you'll be fine if you update your data model on the main queue:

- (void)addRow:(NSString *)data
{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSInteger nextIndex = self.arrayData.count;
        [self.arrayData addObject:data];
        [NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:@{@"insert": @[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
    });
}
like image 98
Timothy Moose Avatar answered Apr 28 '23 09:04

Timothy Moose