Swift: 在 UITableViewCell 中异步加载图像

Swift: load images Async in UITableViewCell

我有一个 tableview 是我用代码创建的(没有 storyboard):

class MSContentVerticalList: MSContent,UITableViewDelegate,UITableViewDataSource {
var tblView:UITableView!
var dataSource:[MSC_VCItem]=[]

init(Frame: CGRect,DataSource:[MSC_VCItem]) {
    super.init(frame: Frame)
    self.dataSource = DataSource
    tblView = UITableView(frame: Frame, style: .Plain)
    tblView.delegate = self
    tblView.dataSource = self
    self.addSubview(tblView)
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: nil)

    let record = dataSource[indexPath.row]
    cell.textLabel!.text = record.Title
    cell.imageView!.downloadFrom(link: record.Icon, contentMode: UIViewContentMode.ScaleAspectFit)
    cell.imageView!.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    print(cell.imageView!.frame)
    cell.detailTextLabel!.text = record.SubTitle
    return cell
}
}

在其他 class 中,我有一个异步下载图像的扩展方法:

   extension UIImageView
{
    func downloadFrom(link link:String?, contentMode mode: UIViewContentMode)
    {
        contentMode = mode
        if link == nil
        {
            self.image = UIImage(named: "default")
            return
        }
        if let url = NSURL(string: link!)
        {
            print("\nstart download: \(url.lastPathComponent!)")
            NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, _, error) -> Void in
                guard let data = data where error == nil else {
                    print("\nerror on download \(error)")
                    return
                }
                dispatch_async(dispatch_get_main_queue()) { () -> Void in
                    print("\ndownload completed \(url.lastPathComponent!)")
                    self.image = UIImage(data: data)
                }
            }).resume()
        }
        else
        {
            self.image = UIImage(named: "default")
        }
    }
}

我在其他地方使用了这个功能并且工作正常,根据我的日志我了解到图像下载没有问题(当单元格被渲染时)并且在图像下载后,单元格 UI 没有更新。

我也尝试过使用像 Haneke 这样的缓存库,但问题仍然存在并且没有改变。

请帮我理解错误

谢谢

设置图片后你应该调用self.layoutSubviews()

编辑:从 setNeedsLayout 更正为 layoutSubviews

通过子类化 UITableViewCell 创建您自己的单元格。您正在使用的样式 .Subtitle 没有图像视图,即使 属性 可用。只有样式 UITableViewCellStyleDefault 具有图像视图。

问题是 UITableViewCell.subtitle 再现将在 cellForRowAtIndexPath returns 后立即布局单元格(覆盖您尝试设置 frame 图像视图)。因此,如果您异步检索图像,单元格将 re-laid 输出,就好像没有图像可显示一样(因为您没有将图像视图的 image 属性 初始化为任何东西),并且当您稍后异步更新 imageView 时,单元格的布局方式已经使您无法看到下载的图像。

这里有几个解决方案:

  1. 您可以让 download 将图像更新为 default,不仅是在没有 URL 时,还有 URL ](因此您首先将其设置为默认图像,然后将图像更新为您从网络下载的图像):

    extension UIImageView {
        func download(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFill, placeholder: UIImage? = nil) {
            contentMode = mode
            image = placeholder
            URLSession.shared.dataTask(with: url) { data, response, error in
                guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
                    print("error on download \(error ?? URLError(.badServerResponse))")
                    return
                }
                guard 200 ..< 300 ~= response.statusCode else {
                    print("statusCode != 2xx; \(response.statusCode)")
                    return
                }
                guard let image = UIImage(data: data) else {
                    print("not valid image")
                    return
                }
                DispatchQueue.main.async {
                    print("download completed \(url.lastPathComponent)")
                    self.image = image
                }
            }.resume()
        }
    }
    

    这确保单元格将根据图像的存在进行布局,无论如何,图像视图的异步更新将起作用(有点:见下文)。

  2. 而不是使用 UITableViewCell 的动态布局 .subtitle 再现,您还可以创建自己的单元格原型,该原型适当地布局并具有固定的图像大小看法。这样,如果没有立即可用的图像,它不会像没有可用图像一样重新格式化单元格。这使您可以使用自动布局完全控制单元格的格式。

  3. 您还可以定义 downloadFrom 方法以获取额外的第三个参数,即下载完成时您将调用的闭包。然后你可以在那个闭包里面做一个 reloadRowsAtIndexPaths 。不过,这假定您修复此代码以缓存下载的图像(例如 NSCache),以便您可以在再次下载之前检查是否有缓存的图像。

话虽如此,正如我上面提到的,这个基本模式存在一些问题:

  1. 如果您向下滚动然后向上滚动,您将re-retrieve 来自网络的图像。您确实希望在再次检索之前缓存以前下载的图像。

理想情况下,您的服务器响应 headers 已正确配置,以便内置 NSURLCache 会为您处理此问题,但您必须对其进行测试。或者,您可以自己将图像缓存在自己的 NSCache.

  1. 如果您快速向下滚动到第 100 行,您真的不希望可见单元格积压在前 99 行不再可见的图像请求之后。您确实想取消对滚动到屏幕外的单元格的请求。 (或者使用dequeueCellForRowAtIndexPath,你re-use个单元格,然后你可以写代码取消之前的请求。)

  2. 如上所述,你真的很想做dequeueCellForRowAtIndexPath这样你就不必不必要地实例化UITableViewCellobjects。你应该重复使用它们。

就我个人而言,我可能建议您 (a) 使用 dequeueCellForRowAtIndexPath,然后 (b) 将其与成熟的 UIImageViewCell 类别之一结合使用,例如 AlamofireImage, SDWebImage, DFImageManager or Kingfisher。执行必要的缓存和取消先前的请求是一项 non-trivial 练习,使用其中一个 UIImageView 扩展将简化您的生活。如果您决定自己执行此操作,您可能还想查看这些扩展的一些代码,这样您就可以 pick-up 了解如何正确执行此操作。

--

例如,使用AlamofireImage,您可以:

  1. 定义自定义 table 视图单元格子class:

    class CustomCell : UITableViewCell {
        @IBOutlet weak var customImageView: UIImageView!
        @IBOutlet weak var customTitleLabel: UILabel!
        @IBOutlet weak var customSubtitleLabel: UILabel!
    }
    
  2. 将单元格原型添加到您的 table 视图故事板,指定 (a) CustomCell 的基础 class; (b) CustomCell 的故事板 ID; (c) 将图像视图和两个标签添加到您的单元格原型,将 @IBOutlets 连接到您的 CustomCell subclass; (d) 添加任何必要的约束来定义图像视图的 placement/size 和两个标签。

    您可以使用自动布局约束来定义图像视图的尺寸

  3. 你的 cellForRowAtIndexPath,然后可以做类似的事情:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
    
        let record = dataSource[indexPath.row]
        cell.customTitleLabel.text = record.title
        cell.customSubtitleLabel.text = record.subtitle
        if let url = record.url {
            cell.customImageView.af.setImage(withURL: url)
        }
    
        return cell
    }
    

    有了它,您不仅可以享受基本的异步图像更新,还可以享受图像缓存、可见图像的优先级排序,因为我们正在重用出队的单元格,它更高效,等等。通过使用带有约束的单元格原型和您的custom table view cell subclass,一切都正确布局,让您无需在代码中手动调整 frame

无论您使用这些 UIImageView 扩展中的哪一个,过程基本相同,但目标是让您摆脱自己编写扩展的困境。

天哪,layoutSubviews不建议直接使用
解决问题的正确方法是调用:
[self setNeedsLayout];
[self layoutIfNeeded];
在这里,这两种方式必须一起调用。
试试这个,祝你好运。

这里更喜欢 SDWebImages 库 link

它将异步下载图片并缓存图片 也很容易集成到项目中