34

我正在使用 GCD 为我的 uitableview 异步下载图像,但是有一个问题 - 滚动图像时一直闪烁和变化。我尝试将每个单元格的图像设置为零,但这并没有太大帮助。快速向上滚动时,所有图像都是错误的。我能做些什么呢?这是我的细胞方法:

- (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;
}
4

8 回答 8

97

这里的问题是您的图像获取块持有对 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;
}
于 2013-03-27T20:05:03.623 回答
7

关键是您没有完全理解单元重用的概念。这与异步下载不太一致。

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}

当请求完成并加载所有数据时执行。但是 cell 在创建块时获取它的值。

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

你应该寻找一种更聪明的方法来做到这一点。有很多教程。谷歌用于延迟图像加载。

于 2013-03-27T20:15:14.183 回答
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;
}
于 2013-03-27T21:13:46.953 回答
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;
}

我希望它可以帮助一些开发人员!干杯。

学分:iOS 7 中的表格视图图像

于 2014-10-01T18:11:12.987 回答
1

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

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

如果您使用的是 AFNetworking 2.0,这可能有点矫枉过正,但它对我来说很有效。

如果您想尝试一下,这里是 github 的链接

于 2014-06-27T08:00:43.483 回答
1

不要重新发明轮子,只需使用这两个很棒的 pod:

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndi​​cator-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];
于 2015-12-17T22:30:32.557 回答
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)
              }
          });
     }
于 2016-10-26T06:47:24.560 回答
0

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

有关更多详细信息,请参阅此答案:异步获取已完成:单元格是否仍在显示?.

这是我的课程:

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)
    }
于 2018-10-26T00:19:50.000 回答