使用completionBlock下载和保存文件异步时,错误的CollectionView单元格图像

时间:2014-03-17 13:02:10

标签: ios objective-c uicollectionview uicollectionviewcell

我意识到已经存在很多这些问题,但似乎没有一个问题可以解决这个问题。

CollectionView从files数组中读取文件名,并从Documents目录中加载图像或下载它们,显示并保存到Documents。工作非常流畅,没有任何额外的库,但是当快速滚动时,几个单元格会收到错误的图像。使用任何操作调用[collectionView reloadData]或向后滚动会将这些错误的图像重新加载到好的图像中。

我认为它与重复使用异步图像分配的单元格有关,但是如何解决这个问题呢?在Storyboard中定义的CollectionView和自定义单元格。图像存储在基本身份验证后面的服务器上,因此大多数重用解决方案都不适用。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    BrowseCollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];

    NSString *fileName = [files objectAtIndex:indexPath.item];
    NSString *filePath = [thumbDir stringByAppendingPathComponent:fileName];

    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath])
    {
        cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
    }
    else //DOWNLOAD
    {
        NSString *strURL = [NSString stringWithFormat:@"%@%@", THUMBURL, fileName];
        NSURL *fileURL = [NSURL URLWithString:strURL];
        cell.imageView.image = [UIImage imageNamed:@"placeholder.jpg"];

        [self downloadFromURL:fileURL to:filePath
              completionBlock:^(BOOL succeeded, UIImage *image) {
                  if (succeeded)
                  {                          
                      cell.imageView.image = image;
                  }
              }];
        }
    }
    cell.imageName = fileName;
    return cell;
}

- (void)downloadFromURL:(NSURL*)url to:(NSString *)filePath completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSString *authStr = [NSString stringWithFormat:@"%@:%@", LOGIN, PASS];
    NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
    NSString *authValue = [authData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadRevalidatingCacheData timeoutInterval:30];

    [request setValue:[NSString stringWithFormat:@"Basic %@", authValue] forHTTPHeaderField:@"Authorization"];

    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
        completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
         if (!error)
         {
             UIImage *image = [[UIImage alloc] initWithData:data];
             [data writeToFile:filePath atomically:NO];
             completionBlock(YES, image);
         } else {
             completionBlock(NO, nil);
         }
     }];
}

我尝试了很多修改,但似乎都没有解决问题。目前,效果是快速滚动期间出现的新单元格被多次更改,其中一些单元格总是以错误的图像结束,指向一些重用问题。

提前感谢您的任何帮助和建议。

3 个答案:

答案 0 :(得分:14)

问题在于,当快速滚动时,在异步请求完成时,单元可能会出列并重新用于另一个单元,从而更新错误的单元。因此,在完成块中,您应确保单元格仍然可见:

[self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image) {
    if (succeeded) {                          
        BrowseCollectionViewCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
        if (updateCell) {                       // if the cell is still visible ...
            updateCell.imageView.image = image; // ... then update its image
        }
    }
}];

请注意,不应将此UICollectionView方法cellForItemAtIndexPath与名称相似的UICollectionViewDataSource方法collectionView:cellForItemAtIndexPath:混淆。如果单元格仍然可见,则UICollectionView方法cellForItemAtIndexPath会返回UICollectionViewCell,如果不可见,则返回nil

顺便说一下,上面假设这个单元格的NSIndexPath不能改变(即,在异步检索图像时,不可能在该行上方插入额外的行)。这有时不是一个有效的假设。因此,如果您想要小心,那么您真正应该做的是返回模型并重新计算此模型对象的NSIndexPath,并在确定适当的updateCell引用时使用它。


理想情况下,一旦解决了上述问题,您可以对此流程进行一些优化:

  1. 如果在上一个请求仍在运行时重复使用该单元格,则可能需要取消该先前请求。如果你没有,你快速滚动,比如200个单元格,现在显示单元格201到221,那么对那20个当前可见单元格的请求将排队,直到前200个请求完成才会显示。

    但是,为了能够取消先前的请求,您无法使用sendAsynchronousRequest。您必须使用可取消的基于代理的NSURLConnectionNSURLSessionTask

  2. 您可能应该缓存图片。如果您当前可见的单元格都有自己的图像,然后将它们滚动并重新打开,则您似乎正在重新请求它们。您应首先查看是否已检索到图像,如果已经检索,请使用该图像,并且只有在缓存中没有图像时才重新发出请求。

    是的,我知道您正在使用持久存储中的文件版本进行缓存,但您也可能希望缓存到RAM。通常人们会为此目的使用NSCache

  3. 这要改变很多。如果您需要帮助,请告诉我们。但更容易的是使用SDWebImageAFNetworking中的UIImageView类别,它会为您完成所有这些。

答案 1 :(得分:13)

您需要在某处保留一些上下文信息。换句话说,在完成块中,检查单元格是否确实包含下载的图像。如果没有,请不要分配它。例如图像URL。你可以这样做......

  • 将属性NSURL *imageURL添加到您的单元格对象
  • 在您的downloadFromURL:...商店fileURLimageURL小区属性之前
  • 完成块中的
  • ,比较fileURL是否与cell.imageURL匹配,如果匹配,则设置图像,否则不设置
  • nil imageURL
  • 中的小区prepareForReuse属性

...或者您可以将加载代码移动到您的单元格类中,并为其分配图像的URL等。许多方法都可以。

答案 2 :(得分:2)

您需要在块替换中获取单元格的引用:

[self downloadFromURL:fileURL to:filePath
              completionBlock:^(BOOL succeeded, UIImage *image) {
                  if (succeeded)
                  {                          
                      cell.imageView.image = image;
                  }
              }];

 [self downloadFromURL:fileURL to:filePath
              completionBlock:^(BOOL succeeded, UIImage *image) {
BrowseCollectionViewCell *innerCell = (BrowseCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
                  if (succeeded)
                  {                          
                      innerCell.imageView.image = image;
                  }
              }];