3
votes

My goal is to set the text, then make an asynchronous call for image, and set the image on arrival.

I currently make an asynchronous call for image to set in UITableViewCell, and noticed my previous technique resulted in a loop I recently identified:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell* cell = [_tableView dequeueReusableCellWithIdentifier:@"id"];

    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"id"];
    }

    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:userAvatar]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (data) {
            UIImage* avatarImage = [UIImage imageWithData:data];
            if (avatarImage) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [cell.imageView setImage:avatarImage];
                });


                /*
                 .--.      [STOP]
          .----'   '--.    |
          '-()-----()-'    |

          If I include the following, 
          the program goes in a loop, 
          and have not readily identified workaround:

                dispatch_async(dispatch_get_main_queue(), ^{
                    [_tableView reloadData];
                });

                           [GO]           .--.  
                           |         .----'   '--. 
                           |         '-()-----()-' 

                */
            }
        }
    }];

    cell.textLabel.text = @"foo";
    cell.detailTextLabel.text = @"bar";
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;


    return cell;
}

Is there a methodology to produce the outcome I am looking for without a custom setter in custom class?

1
DO NOT call reloadData from cellForRow.... Bad things happen. Look at Apple's LazyTableImages sample app to see how to do this properly.rmaddy
You can also use AFNetworking to do that easily - check the UIImageView+AFNetworking.h additions.joao
@joao AFNetworking uses a very simplistic approach which basically "makes it work" but actually introduces a more serious problem. So, I wouldn't recommend it.CouchDeveloper
@CouchDeveloper can you tell me what is the problem? I've been using AFNetworking for some time and never got any strange behaviour.joao
@joao The fundamental problem with a simplistic approach is that it can happen that requests (for loading images) will be started and cancelled rapidly - depending how fast a user scrolls the table view. In a worst case, this can start hundred requests per second which which will be fully served by the web service but cancelled immediately afterward by the client, which never receives the data and never caches it locally.CouchDeveloper

1 Answers

3
votes

You definitely don’t want to call -reloadData every time you load an image, it’s way too heavyweight, and also causes all the cells to be reloaded, which calls this method, as you’ve discovered.

I’m not clear why you want to call -reloadData at all, because this should already work as-is—setting the image on the imageView of the cell will cause the display to refresh...EXCEPT:

There’s a big logic error in this code because cells are re-used all the time, but you’re asynchronously loading an image and then setting it to a particular cell. There’s no guarantee that the cell will still be assigned to the same row of the table by the time the image loads—you’re just setting an image on a random cell at that point.

In general, tableViews only create just enough cells as can be visible on screen at the same time, not one per row of the table. As you scroll down through a table the cells from the top are removed and re-used down below, to save memory and creation time.

Instead of setting the image on the cell directly, look up the cell again in the tableView by its row (if it’s still visible) and set it that way.

Also, you’re not caching the images you fetch, which will result in a new fetch every time the user scrolls down and then up again. While the built-in networking code will do some level of caching, you never know how much it is and it’s certainly not speedy to change HTTP requests into full images, so you want to cache those images yourself, associated with the original URLs or the rows or whatever keys uniquely identify them.