Images downloaded asynchronously only appear in UI

2019-01-22 14:58发布

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?

5条回答
Emotional °昔
2楼-- · 2019-01-22 15:10

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.

查看更多
Root(大扎)
3楼-- · 2019-01-22 15:17

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.

查看更多
Juvenile、少年°
4楼-- · 2019-01-22 15:20

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];
查看更多
Ridiculous、
5楼-- · 2019-01-22 15:30

this is my solution, using a category for UIImageView.
NOTE: since we perform self.image = nil in the first line, you must set a placeholder image for the cell.ImageView after calling this method.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
   [cell.imageView loadImageForURLString:row.imageUrl];
   cell.imageView.image = tempImage;
...
}

and the category:

#import "UIImageView+AsyncLoad.h"

@implementation UIImageView (AsyncLoad)

- (void)loadImageForURLString:(NSString *)imageUrl
{
    self.image = nil;

    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response, NSData * data, NSError * error)
     {
         [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
         if (data && self.window) {
             self.image = [UIImage imageWithData:data];
         }
     }];
}

@end
查看更多
乱世女痞
6楼-- · 2019-01-22 15:35

I'm pretty late to the party, but if you dig a bit deeper in the UIImageView+AFNetworking docs you'll find the method – setImageWithURLRequest:placeholderImage:success:failure: which you can use to reload the cell when the image is available:

NSURLRequest *urlRequest = [NSURLRequest requestWithURL: [NSURL URLWithString: imageURL]];
__weak UITableViewCell *weakCell = cell;

[cell.imageView setImageWithURLRequest: urlRequest
                      placeholderImage: nil
                               success: ^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {

                                   __strong UITableViewCell *strongCell = weakCell;
                                   strongCell.imageView.image = image;

                                   [tableView reloadRowsAtIndexPaths: @[indexPath]
                                                    withRowAnimation: UITableViewRowAnimationNone];

                               } failure: NULL];
查看更多
登录 后发表回答