简体   繁体   中英

TableView Async Refresh on iOS

I have a simple TableViewController populated with this method:

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

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSInteger row = [indexPath row];
        cell.textLabel.text = [[NSString alloc] initWithFormat:@"%d", row]; 

        dispatch_async(dispatch_get_main_queue(), ^{

            [cell setNeedsLayout];

        });

    }); 

    return cell;
}

On the first load, numbers are correctly diplayed. When I scoll the view, numbers are displayed random (then they are sorted correctly).

Why, scrolling, numbers are not sorted?

在此处输入图片说明

UIKit is not thread safe. You cannot set an image of a UIImageView on another thread, in the same way, you cannot set the text of a UILabel on another thread. Try this instead:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSInteger row = [indexPath row];
    [cell.textLabel  performSelectorOnMainThread:@selector(setText:) withObject:[NSString stringWithFormat:@"%d", row] waitUntilDone:YES];
}); 

Personally, though, I don't understand why you are using dispatch at all. The overhead to create a string from an integer is minimal, unless there is more processing going on behind the scenes than what you have shown here.

You could just put this instead of the dispatch call instead:

 NSInteger row = [indexPath row];
 cell.textLabel.text = [NSString stringWithFormat:@"%i", row];

I think from reading about what you're trying to do you are slightly going about it the wrong way. You shouldn't be doing asynchronous stuff in cellForRowAtIndexPath because you can't be sure that the cell you're updating later on is the right one.

Consider your code:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    ...

    return cell;
}

So you're creating a cell and returning it. It might be one from the reuse queue or a new one. So far so good. But then you add in this code:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSInteger row = [indexPath row];
        cell.textLabel.text = [[NSString alloc] initWithFormat:@"%d", row]; 
        dispatch_async(dispatch_get_main_queue(), ^{
            [cell setNeedsLayout];
        });
    }); 

So you're now dispatching onto a background queue to set something on that cell. But what happens if these course of events happen:

  1. Table asks for a cell.

  2. New cell is created, cell A, dispatch gets done to set stuff later.

  3. User scrolls and cell A gets taken off the view and put into reuse queue.

  4. Table asks for a cell.

  5. Cell A is returned from reuse pool and another dispatch is put on queue.

  6. First dispatch finishes, setting things on cell A.

That's not what you want since now cell A has old data on it. Granted, if your queue is serial you might be able to get away with it as the 2nd dispatch will always finish after the first, but a) that's not nice as stale data will be seen anyway and b) your queue might be a parallel queue anyway.

So, what's the right solution you ask? Well, my usual approach to this is to have a custom cell. It sounds like you want to have the cell load an image and I'm assuming you want to load that image from the web? So my way of doing it is to have a custom cell with a loadImage method on it which is called on each visible cell when the table view has either stopped scrolling and is not decelerating, or the table view has stopped decelerating. (See UIScrollViewDelegate methods for what I mean there).

Then in the loadImage method I fire off an HTTP request to get the image and wait for it to return and set the UIImageView in my custom cell to the image returned. The crucial extra bit is that I have a setter on the custom cell to set the object / URL / whatever the cell is displaying which gives the information about which image to download. Then when the cell gets a request for a new URL it cancels the old one and starts the new one in the next loadImage call.

Hopefully that makes sense! I actually do it slightly differently, but that's as simple a description as I can give of the general idea. I don't have any code to show unfortunately but please do ask any questions and I'll attempt to pad it out some more.

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