使用GCD异步下载UITableView的图像

时间:2013-03-27 19:52:21

标签: ios objective-c ios6

我正在使用GCD异步下载我的uitableview图像,但是有一个问题 - 当滚动图像时闪烁并且一直在变化。我尝试将每个单元格的图像设置为nil,但它没有多大帮助。 快速向上滚动时,所有图像都是错误的。我该怎么办? 这是我的细胞方法:

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {

                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];

                UIImage* image = [[UIImage alloc] initWithData:imageData];

                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                    [cell setNeedsLayout];
                     });
            });

    cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }
    return cell;
}

8 个答案:

答案 0 :(得分:95)

这里的问题是你的图像获取块正在保持对tableview单元格的引用。下载完成后,即使您已经回收单元格以显示不同的行,它也会设置imageView.image属性。

在设置图像之前,您需要下载完成块来测试图像是否仍然与图像相关。

还值得注意的是,您不会将图像存储在单元格以外的任何位置,因此每次在屏幕上滚动行时,您都会再次下载它们。您可能希望将它们缓存在某处并在开始下载之前查找本地缓存的图像。

编辑:这是一种使用单元格的tag属性进行测试的简单方法:

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

    cell.tag = indexPath.row;
    NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
    if (parsedData)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^(void) {

            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];

            UIImage* image = [[UIImage alloc] initWithData:imageData];
            if (image) {
                 dispatch_async(dispatch_get_main_queue(), ^{
                     if (cell.tag == indexPath.row) {
                         cell.imageView.image = image;
                         [cell setNeedsLayout];
                     }
                 });
             }
        });

        cell.textLabel.text = parsedData[@"id"];
    }
    return cell;
}

答案 1 :(得分:7)

关键是你没有完全理解细胞重用概念。这与异步下载不太一致。

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}
请求完成并加载所有数据后执行

。但是当创建块时,单元格会获得它的值。

当执行块时,单元仍然指向现有单元之一。但很可能用户继续滚动。在此期间重复使用单元对象,并且图像与重复使用并分配和显示的“”单元相关联。在此之后不久,除非用户进一步滚动,否则加载并分配和显示正确的图像。等等等等。

你应该寻找一种更聪明的方法。有很多turorials。谷歌为懒人图像加载。

答案 2 :(得分:6)

使用索引路径获取单元格。如果它不可见,则单元格将为nil,您将不会遇到问题。当然,您可能希望在下载数据时缓存数据,以便在您拥有图像时立即设置单元格的图像。

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {
                //  You may want to cache this explicitly instead of reloading every time.
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
                UIImage* image = [[UIImage alloc] initWithData:imageData];
                dispatch_async(dispatch_get_main_queue(), ^{
                    // Capture the indexPath variable, not the cell variable, and use that
                    UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
                    blockCell.imageView.image = image;
                    [blockCell setNeedsLayout];
                });
            });
        cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }

    return cell;
}

答案 3 :(得分:3)

我一直在研究这个问题,我通过自定义UITableViewCell找到了一个很好的方法。

#import <UIKit/UIKit.h>

@interface MyCustomCell : UITableViewCell

@property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask;
@property (nonatomic, weak) IBOutlet UIImageView *myImageView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

现在,在TableViewController中,为NSURLSessionConfiguration和NSURLSession声明两个属性,并在ViewDidLoad中初始化它们:

@interface MyTableViewController ()

@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
@property (nonatomic, strong) NSURLSession *session;
.
.
.
@end

@implementation TimesVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:_sessionConfig];
}

.
.
.

让我们假设您的数据源是NSMutableDictionary的数组(或NSManagedObjectContext)。您可以通过缓存轻松下载每个单元格的图像:

.
.
.
- (MyCustomCell *)tableView:(UITableView *)tableView
   cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

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

    NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row];    

    if (cell.imageDownloadTask)
    {
        [cell.imageDownloadTask cancel];
    }

    [cell.activityIndicator startAnimating];
    cell.myImageView.image = nil;

    if (![myDictionary valueForKey:@"image"])
    {
        NSString *urlString = [myDictionary valueForKey:@"imageURL"];
        NSURL *imageURL = [NSURL URLWithString:urlString];
        if (imageURL)
        {
            cell.imageDownloadTask = [_session dataTaskWithURL:imageURL
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
            {
                if (error)
                {
                    NSLog(@"ERROR: %@", error);
                }
                else
                {
                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                    if (httpResponse.statusCode == 200)
                    {
                        UIImage *image = [UIImage imageWithData:data];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [myDictionary setValue:data forKey:@"image"];
                            [cell.myImageView setImage:image];
                            [cell.activityIndicator stopAnimating];
                        });
                    }
                    else
                    {
                        NSLog(@"Couldn't load image at URL: %@", imageURL);
                        NSLog(@"HTTP %d", httpResponse.statusCode);
                    }
                }
            }];

            [cell.imageDownloadTask resume];
        }
    }
    else
    {
        [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]];
        [cell.activityIndicator stopAnimating];
    }

    return cell;
}

我希望它可以帮助一些开发者! 欢呼声。

信用:Table View Images in iOS 7

答案 4 :(得分:1)

您是否考虑过使用SDWebImage?它也异步下载给定URL中的图像。我没有使用整个库(仅导入UIImageView + WebCache.h)。导入后,您所要做的就是调用方法:

[UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]];

如果您正在使用AFNetworking 2.0,那可能会有点过分,但它对我来说很有用。

Here is the link to the github if you want to give it a try

答案 5 :(得分:1)

不要重新发明轮子,只需使用这几个很棒的吊舱:

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage

简单如下:

    [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]]
                          placeholderImage:nil
                                 completed:^(UIImage *image,
                                             NSError *error,
                                             SDImageCacheType cacheType,
                                             NSURL *imageURL)
     {
         event.image = UIImagePNGRepresentation(image);
     } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];

答案 6 :(得分:1)

除了 Seamus Campbell 的接受答案之外,您还应该知道有时这不起作用。在那种情况下,我们应该重新加载特定的单元格。所以

if (image) {
     dispatch_async(dispatch_get_main_queue(), ^{
          if (cell.tag == indexPath.row) {
               cell.imageView.image = image;
               [cell setNeedsLayout];
          }
      });
 }

应该改成,

    if (image) {
         dispatch_async(dispatch_get_main_queue(), ^{
              if (cell.tag == indexPath.row) {
                   cell.imageView.image = image;
                   self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
              }
          });
     }

答案 7 :(得分:0)

对于那些担心索引路径更改且不一致的人,一种潜在的解决方案是将UITableViewCell或UICollectionViewCell子类化,并添加一个名为stringTag之类的实例变量。然后,将要下载的照片的URL放在stringTag中。设置图像时,请检查stringTag是否仍然是正确的URL。

有关更多详细信息,请参见以下答案:Asynchronous Fetch completed: Is cell still being displayed?

这是我的课程:

import Foundation
import UIKit

class ImageAsyncCollectionViewCell : UICollectionViewCell {
  var stringTag: String?
}

,然后在使用单元格时:

    cell.stringTag = photoKey
    cell.imageView.image = self.blankImage
    if ImageCache.default.imageCachedType(forKey: photoKey).cached {
      ImageCache.default.retrieveImage(forKey: photoKey, options: nil) {
        image, cacheType in
        if let image = image {
          DispatchQueue.main.async {
            if cell.stringTag == photoKey {
              cell.imageView.image = image
            }
          }
        } else {
          print("Doesn't exist in cache, but should")
          self.setCellWithPhoto(photoKey: photoKey, cell: cell)
        }
      }
    } else {
      self.setCellWithPhoto(photoKey: photoKey, cell: cell)
    }