简体   繁体   中英

How do we sync numberOfRowsInSection to cellForRowAtIndexPath?

Our UITableView returns the number of rows for a given section. What we're experiencing is by the time cellForRowAtIndexPath is called, the number has changed so we end up getting array index out of bounds.

Is there a good way of syncing these two methods so the underlying data is not changed? We considered using @synchronized, but not sure when you would release the lock.

One other thing we're doing to refresh the table is this from a separate thread.

[self addUsers:usersToShow];       
[[self invokeOnMainThreadAndWaitUntilDone:NO] refresh:self]; // is this the issue?


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.users.count;  // returns 10 for example
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *newCell= nil;

    static NSString *cellId = @"cellId";

    cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"MyCell" owner:self options:nil];
        cell = newCell;
        self.newCell = nil;
    }

    User* user = [self.users objectAtIndex:indexPath.row]; // index out of bounds now because number of users has changed
}

Even though brain has already answered, I want to emphasize "update model in main thread" with example code.

You might encounter the problem because your model changed in some background thread. The timeline should look like this:

{NSThread number = 1, name = main} -[ViewController tableView:numberOfRowsInSection:] (return 10 for example)

{NSThread number = 8, name = (null)} -[ViewController changeTheModel] (remove some objects from model, or get a new model with less than 10 objects)

{NSThread number = 1, name = main} -[ViewController tableView:cellForRowAtIndexPath:] (get the index out of bound exception because 10th object does not exist)

To solve the problem, you should do something like this when you change your model:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSArray* items = [self getNewModel];// get new model on background thread
    dispatch_async(dispatch_get_main_queue(), ^ {
        self.items = items;// replace the model with new one on main thread
        [self.tableView reloadData];// refresh table without index out of bound exception
    });
});

Hope this could help you. :)

As you've kind of worked out for yourself, you can't use @synchronized because the scope extends beyond the method.

You don't want to be trying to use a Lock object, locking it in numberOfRowsInSection and unlocking it cellForRowAtIndexPath. It's not going to work. What you need to do is ensure that you do any locking you need to in cellForRowAtIndexPath and handle the fact that the row passed in might not be valid anymore eg:

User * user = nil;    
@synchronized(self.users) {
        if (indexPath.row < [self.users count]) {
             user = [self.users objectAtIndex:indexPath.row];
        }

}

if (user) {
    //configure cell
}
else {
    //set cell fields to be blank
}

Have you tried updating the model (self.users) only in the main thread? This should reduce the likely hood of your updates to the model from interleaving with calls to getNumberOfRows and configureCellAt. In the example you've given you are updating the model on a random thread and then reloading the data on the main thread. I'd recommend ensuring that you're model is thread safe (or updated/read in only the main thread).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM