ios - 从 UITableView 单元格内的 url 异步加载图像 - 滚动时图像更改为错误图像

标签 ios objective-c cocoa-touch uitableview

我已经编写了两种方法来异步加载我的 UITableView 单元格中的图片。在这两种情况下,图像都可以正常加载,但是当我滚动表格时,图像会更改几次,直到滚动结束并且图像将返回到正确的图像。我不知道为什么会这样。

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_async(kBgQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
                                                       @"http://myurl.com/getMovies.php"]];
        [self performSelectorOnMainThread:@selector(fetchedData:)
                               withObject:data waitUntilDone:YES];
    });
}

-(void)fetchedData:(NSData *)data
{
    NSError* error;
    myJson = [NSJSONSerialization
              JSONObjectWithData:data
              options:kNilOptions
              error:&error];
    [_myTableView reloadData];
}    

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    // Return the number of rows in the section.
    // Usually the number of items in your array (the one that holds your list)
    NSLog(@"myJson count: %d",[myJson count]);
    return [myJson count];
}
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

        myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
            cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
        }

        dispatch_async(kBgQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];

            dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
            });
        });
         return cell;
}

... ...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

            myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
            if (cell == nil) {
                cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
            }
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
    NSURLRequest* request = [NSURLRequest requestWithURL:url];


    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response,
                                               NSData * data,
                                               NSError * error) {
                               if (!error){
                                   cell.poster.image = [UIImage imageWithData:data];
                                   // do whatever you want with image
                               }

                           }];
     return cell;
}

最佳答案

假设您正在寻找快速的战术修复,您需要做的是确保单元格图像已初始化并且单元格的行仍然可见,例如:

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

    cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];

    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];

    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            UIImage *image = [UIImage imageWithData:data];
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.poster.image = image;
                });
            }
        }
    }];
    [task resume];

    return cell;
}

上面的代码解决了由于单元被重用的事实而产生的一些问题:

  1. 在启动后台请求之前,您没有初始化单元格图像(这意味着在下载新图像时,出列单元格的最后一张图像仍然可见)。确保 nil 任何 ImageView 的 image 属性,否则您会看到图像闪烁。

  2. 一个更微妙的问题是,在一个非常慢的网络上,您的异步请求可能不会在单元格滚出屏幕之前完成。您可以使用 UITableView 方法 cellForRowAtIndexPath: (不要与名称相似的 UITableViewDataSource 方法 tableView:cellForRowAtIndexPath:) 查看该行的单元格是否仍然可见。如果单元格不可见,此方法将返回 nil

    问题在于,当您的异步方法完成时,单元格已滚动,更糟糕的是,该单元格已被重新用于表格的另一行。通过检查该行是否仍然可见,您可以确保不会意外地使用已滚动离开屏幕的行的图像来更新图像。

  3. 与手头的问题有点无关,我仍然觉得有必要更新它以利用现代约定和 API,特别是:

    • 使用 NSURLSession 而不是将 -[NSData contentsOfURL:] 分派(dispatch)到后台队列;

    • 使用 dequeueReusableCellWithIdentifier:forIndexPath: 而不是 dequeueReusableCellWithIdentifier: (但请确保使用单元原型(prototype)或注册类或 NIB 作为该标识符);和

    • 我使用了符合 Cocoa naming conventions 的类名(即以大写字母开头)。

即使进行了这些更正,也存在一些问题:

  1. 上面的代码没有缓存下载的图片。这意味着如果您在屏幕上滚动图像并返回屏幕,应用程序可能会尝试再次检索图像。也许您会很幸运,您的服务器响应 header 将允许 NSURLSessionNSURLCache 提供的相当透明的缓存,但如果没有,您将发出不必要的服务器请求并提供更慢的用户体验。

  2. 我们不会取消对滚动到屏幕外的单元格的请求。因此,如果您快速滚动到第 100 行,则该行的图像可能会积压在对前 99 行的请求之后,这些请求甚至不再可见。您总是希望确保优先考虑对可见单元格的请求以获得最佳用户体验。

解决这些问题的最简单方法是使用 UIImageView 类别,例如 SDWebImage 提供的类别。或 AFNetworking .如果您愿意,您可以编写自己的代码来处理上述问题,但工作量很大,而且上面的 UIImageView 类别已经为您完成了。

关于ios - 从 UITableView 单元格内的 url 异步加载图像 - 滚动时图像更改为错误图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16663618/

相关文章:

ios - 如何将浮点值放入 UIAlertView

c++ - 在不同的线程上发送和接收

iphone - NSMutableArray removeObjectAtIndex : Behaviour

cocoa-touch - 调整大小后调整 UIPopoverController 位置

ios - Remote Config A/B Test 不在 iOS 上提供结果

ios - 如何在 UITableViewCell 中添加交互式 View Controller ?

ios - Xcode 无法为 ios 13.3.1 设备构建 flutter 代码,但在 13.3 ios 模拟器上运行代码

ios - 使用 convertRect 获取 subview 的窗口坐标问题

ios - 需要有关多个 subview Controller 的建议

objective-c - 为什么这个 subview 没有被删除?