ericbae
ericbae

Reputation: 9644

Objective C - UITableViewCell loading image asynchronously

I am displaying a table. Each row has an image icon, loaded from an URL.

Since downloading images synchronously blocks the UI, I've implemented am asynchronous way via grand central dispatch.

My problem is that when I scroll down and up, since cells are being re-used, the incorrect images show up.

I can guess why this is happening - it's because the re-used cells update the image and therefore, previous cells will now have the newly downloaded, and wrong, image. What would be an ideal way to resolve this?

Here's my code.

For each image downloaded, I'm storing it in a singleton class called "ImageStore".

// set the data for each cell - reusing the cell
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];

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

    // setting the image for each cell
    // first check if there is UIImage in the ImageStore already
    NSString *imageUrl = [obj objectForKey:@"image"];
    if (imageUrl) {
      if ([[ImageStore sharedStore] imageForKey:imageUrl]) {

        [[[tableView cellForRowAtIndexPath:indexPath] imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
      } else {

        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
      UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
      [NSURL URLWithString:[obj objectForKey:@"image"]]]];
      dispatch_sync(dispatch_get_main_queue(), ^{
        [[ImageStore sharedStore]setImage:image forKey:imageUrl];
        [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationNone];

            [[[tableView cellForRowAtIndexPath:indexPath] imageView] setImage:image];
      });
    });
      }
    }
    return cell;
}

Upvotes: 2

Views: 4111

Answers (4)

jsetting32
jsetting32

Reputation: 1632

Take a look at AFNetworking... They make dealing with network callbacks EASY!!! I recommend AFNetworking over ASIHTTPRequest because AFNetworking is keeping updated and ASIHTTPRequest is not... they kinda just stopped developing.

Here is an example of how to use AFNetworking to download images asynchronously:

NSDictionary * object = [self.array objectAtIndex:indexPath.row];
NSDictionary * image = [object valueForKey:@"image"];
NSString *imageUrl = [image valueForKeyPath:@"url"];
NSURL *url = [NSURL URLWithString:imageUrl];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"empty-profile-150x150.png"]];

The setImageWithURL: placeholderImage: method is what I used to do this... Rather than multiple methods and lines of code, I accomplish everything with this line.

This entire line of code does exactly what you want to do. There is really no need to recreate the wheel :). It does help though going through the lower level of programming to really understand the implementation of what is REALLY going on under the hood.

View the link to download the library and view more examples on how to use it... Seriously, it makes your life alot easier not having to worry THAT MUCH about threads and GCD.

AFNetworking Demo

I haven't dived into the code of AFNetworking but my application runs like BUTTER when loading the images into the cells. It looks great :)

Oh and here the docs for AFNetworking: AFNetworking Documentation

Upvotes: 2

Paresh Navadiya
Paresh Navadiya

Reputation: 38259

Try this:

NSString *imageUrl = [obj objectForKey:@"image"];
if (imageUrl) {
  if ([[ImageStore sharedStore] imageForKey:imageUrl]) {
    [[cell imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
  } else {
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
  dispatch_async(queue, ^{
     UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
     [NSURL URLWithString:[obj objectForKey:@"image"]]]];
  dispatch_sync(dispatch_get_main_queue(), ^{
     [[cell imageView] setImage:image];
     [cell setNeedsLayout];
  });
  });
}

EDIT : Check this grand-central-dispatch-for-ios-lazy-loading for tableview lazy loading

Upvotes: 4

ipraba
ipraba

Reputation: 16553

A little bit of modification from Prince Answer

NSString *imageUrl = [obj objectForKey:@"image"];

if (imageUrl) 
{
  if ([[ImageStore sharedStore] imageForKey:imageUrl]) 
  {
      //This condition means the current cell's image has been already downloaded and stored. So set the image to imageview
      [[cell imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
  } 
  else 
  {
      //While reusing this imageView will have previous image that will be visible till the image is downloaded. So i am setting this image as nil.
      [[cell imageView] setImage:nil]; 

      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
      dispatch_async(queue, ^{

         //Called Immediately.
         UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
         [NSURL URLWithString:[obj objectForKey:@"image"]]]];
         dispatch_sync(dispatch_get_main_queue(), ^{

           //Called when the image is downloaded
           //Store in any external object. So that next time reuse this will not be downloaded
           [[ImageStore sharedStore]setImage:image forKey:imageUrl];
           //Also set the image to the cell
           [[cell imageView] setImage:image];
           [cell setNeedsLayout];
        });
     });
  }
}

Upvotes: 2

sqreept
sqreept

Reputation: 5544

I've noticed this behavior, too.

The only general solution I could find is to disable cell reuse.

Upvotes: -1

Related Questions