使用 Autolayout 在 UITableView 中异步加载图像

Posted

技术标签:

【中文标题】使用 Autolayout 在 UITableView 中异步加载图像【英文标题】:Loading images asynchronously in UITableView with Autolayout 【发布时间】:2015-08-16 14:41:08 【问题描述】:

我有一个 UITableView,它在 UITableView 中异步加载图像。由于所有图片的高度不同,所以我根据图片的高度设置了一个高度约束。

当我使用 DataWithContentsOfUrl 时,这可以正常工作,但是会冻结 UI。当我异步加载时,图像会像这样堆叠在一起:

http://i.stack.imgur.com/TEUwK.png

我使用的代码如下:

NSURL * imageURL = [NSURL URLWithString:img];
NSURLRequest* request = [NSURLRequest requestWithURL:imageURL];
cell.imageViewHeightConstraint.constant=0;
[NSURLConnection sendAsynchronousRequest:request    queue:[NSOperationQueue mainQueue]  completionHandler:^(NSURLResponse * response,   NSData * data,  NSError * error) 
    if (!error)
        UIImage *imgz = [UIImage imageWithData:data];
        [cell.CellImg setBackgroundImage:imgz forState:UIControlStateNormal];
        [cell.CellImg sizeToFit];
        cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
        [cell setNeedsUpdateConstraints];
    
];
[cell updateConstraints];

当我上下滚动几次时,图像会正确定位自己。

【问题讨论】:

【参考方案1】:

该问题未指定不同图像大小的所需行为。单元格应该变得更高以适应,还是只是单元格内的图像视图(看起来像代码中的按钮)?

但是我们应该暂时搁置这个问题,并处理代码中更严重的问题:它从cellForRowAtIndexPath 发出一个无人看管的网络请求。结果,(a)用户来回滚动将产生许多冗余请求,并且(b)用户快速滚动很长一段路将生成一个请求,该请求在启动它的单元格消失时被满足 - 重用为单元格换另一行。

为了解决 (a),数据源应该缓存获取的图像,并且只请求那些尚未收到的图像。为了解决 (b),完成块不应直接引用单元格。

一个简单的缓存应该是这样的:

@property(strong,nonatomic) NSMutableDictionary *images;

// initialize this when you initialize your model
self.images = [@ mutableCopy];

// move the network code into its own method for clarity
- (void)imageWithPath:(NSString *)path completion:(void (^)(UIImage *, NSError *))completion 
    if (self.images[indexPath]) 
        return completion(self.images[indexPath], nil);
    
    NSURL *imageURL = [NSURL URLWithString:path];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) 
        if (!error)
            UIImage *image = [UIImage imageWithData:data];
            self.images[indexPath] = image;
            completion(image, nil);
         else 
            completion(nil, error);
        
    ];

现在,我们通过首先检查 cellForRowAtIndexPath 中缓存中的图像来解决多请求问题。

UIImage *image = self.images[indexPath];
if (image) 
    [cell.CellImg setBackgroundImage:image forState:UIControlStateNormal];
 else 
    // this is a good place for a placeholder image if you want one
    [cell.CellImg setBackgroundImage:nil forState:UIControlStateNormal];
    // presuming that 'img' is a string from your mode
    [self imageWithPath:img completion:^(UIImage *image, NSError *error) 
        // the image is ready, but don't assign it to the cell's subview
        // just reload here, so we get the right cell for the indexPath
        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    ];

还要注意在完成块中没有做什么......我们通过不引用单元来修复单元重用。相反,知道图像现在已缓存,我们重新加载 indexPath。

回到图像大小:大多数应用程序喜欢看到表格视图单元格随着可变高度子视图变高或变短。如果是这种情况,那么您根本不应该对该子视图施加高度限制。相反,将其顶部和底部边缘约束到单元格的内容视图(或将其包含在子视图链中,这些子视图相互约束顶部和底部并包含单元格的顶部和底部边缘的最顶部和最底部子视图)。然后(我认为在 ios 5+ 中),这将允许您的单元格使用该子视图约束链更改高度...

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = // your best guess at the average height here

【讨论】:

我确实有一个 NSCache,只是没有显示它,因为我认为这不是错误背后的原因。【参考方案2】:

尝试以下和约束常量:

cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
[table reloadRowsAtIndexPaths:@[indexPathForCurrentCell] withRowAnimation:UITableViewRowAnimationNone];

也可以设置动画。

【讨论】:

【参考方案3】:

虽然reloadRowsAtIndexPaths 有效,但它最终会导致界面变慢且滞后,尤其是在用户滚动时。我的解决方案是,一旦加载数据以迭代 https://graph.facebook.com/v2.1/%@/?access_token=%@&fields=attachments 以获取对象 ID,然后将图像高度尺寸存储在数组中。然后,在cellForRowAtIndexPath 中,只需将cell.imageViewHeightConstraint.constant 设置为数组中的值。这样一来,所有单元格的高度都会在单元格加载后立即设置,并且图像在加载后立即就位。

【讨论】:

以上是关于使用 Autolayout 在 UITableView 中异步加载图像的主要内容,如果未能解决你的问题,请参考以下文章

UITableView!别再用代码计算行高了

代码适配(Autolayout)

AutoLayout

使用AutoLayout布局适配时,如何提前获得AutoLayout完成适配后的子控件的真实frame

自动布局AutoLayout

如何在 Xcode 6.3 中使用 AutoLayout 创建 ScrollView