Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Images downloaded asynchronously only appear in UITableView after tap or scroll

I am successfully loading thumbnail images from blog posts asynchronously into my UITableView.

The issue I'm having is that the images only appear if I tap the cell OR if I scroll down.

When I tap the cell, the image appears on the left, pushing the Title and Subtitle to the right.

When I scroll down, the images appear where they should in the cells as they are revealed.

Here's my code (I'm using AFNetworking):

#import "UIImageView+AFNetworking.h"

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return posts.count;
}

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

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

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

    NSDictionary *post           = [posts objectAtIndex:indexPath.row];
    NSString     *postpictureUrl = [post objectForKey:@"picture"];

    [cell.imageView setImageWithURL:[NSURL URLWithString:postpictureUrl]];

    cell.textLabel.text       = [post objectForKey:@"post_text"];
    cell.detailTextLabel.text = [post objectForKey:@"post_author_name"];
    return cell;
}

I am seeing this in the iPhone 6.0 simulator, XCode 4.5, OSX MtLion.

Any ideas why the images are not drawn on the initial screen?

like image 962
pepe Avatar asked Sep 29 '12 18:09

pepe


3 Answers

The thing you want to be aware of when mixing asynch and tables is that the asynch finishes at an unknown time in the future, possibly after the cell is scrolled away, removed, reused, etc.

Also, the image that gets pulled from the web is lost if that cell is scrolled away. Not sure if AFNetworking caches for you, but it might be better to not assume. Here's a solution using native networking:

// ...
NSDictionary *post           = [posts objectAtIndex:indexPath.row];
NSString     *postpictureUrl = [post objectForKey:@"picture"];

// find a place in your model, or add one, to cache an actual downloaded image
UIImage      *postImage      = [post objectForKey:@"picture_image"];

if (postImage) {
    cell.imageView.image = postImage;   // this is the best scenario: cached image
} else {
    // notice how we don't pass the cell - we don't trust its value past this turn of the run loop
    [self asynchLoad:postpictureUrl forIndexPath:indexPath];
    cell.imageView.image = [UIImage imageNamed:@"default"];
}
// ...

Now, a no-nonsense asynch load without any 3rd party help

- (void)asynchLoad:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath {

    NSURL *url = [NSURL urlWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        if (!error) {

            // create the image
            UIImage *image = [UIImage imageWithData:data];

            // cache the image
            NSDictionary *post = [posts objectAtIndex:indexPath.row];
            [post setObject:image forKey:@"picture_image"];

            // important part - we make no assumption about the state of the table at this point
            // find out if our original index path is visible, then update it, taking 
            // advantage of the cached image (and a bonus option row animation)

            NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
            if ([visiblePaths containsObject:indexPath]) {
                NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
                [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation: UITableViewRowAnimationFade];
                // because we cached the image, cellForRow... will see it and run fast
            }
        }
    }];
}

For this to work, the posts should be created as NSMutableDictionary...

// someplace in your code you add a post to the posts array.  do this instead.

NSDictionary *postData = // however you get a new post
[posts addObject:[NSMutableDictionary dictionaryWithDictionary:postData]];

Alternatively, if it's hard to change the posts model directly, you can setup another structure to cache the downloaded images. A mutable dictionary keyed by the url strings is a good structure to use:

@property (nonatomic,strong) NSMutableDictionary *imageCache;
@synthesize imageCache=_imageCache;

// lazy init on the getter...

- (NSMutableDictionary *)imageCache {
    if (!_imageCache) {
        _imageCache = [NSMutableDictionary dictionary];
    }
    return _imageCache;
}

Now, when configuring the cell, see if there's a cached image by checking the cache...

// change to the cellForRowAtIndexPath method
NSString *postpictureUrl = [post objectForKey:@"picture"];
UIImage  *postImage = [self.imageCache valueForKey:postpictureUrl];

And once an image is downloaded, cache it...

// change to the asynchLoad: method I suggested
UIImage *image = [UIImage imageWithData:data];
[self.imageCache setValue:image forKey:urlString];
like image 52
danh Avatar answered Oct 22 '22 18:10

danh


The issue is resolved by putting a placeholder in this line

...
[cell.imageView setImageWithURL:[NSURL URLWithString:postpictureUrl] placeholderImage:[UIImage imageNamed:@"default"]];
....

The placeholder needs to have dimension ratio similar to the thumbnail to avoid distortion.

like image 5
pepe Avatar answered Oct 22 '22 16:10

pepe


I scratched my head for long and finally figured it out.

My mistake was that I was setting image in cell.imageView when I should be setting my actual outlet cell.eventImageView. It was messing with the generic imageview provided in UITableViewCell. Hope it helps somebody.

like image 2
atulkhatri Avatar answered Oct 22 '22 18:10

atulkhatri