Swift:在UITableViewCell中加载图像Async

Imr*_*deh 10 uitableview ios swift haneke

我有一个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
}
}
Run Code Online (Sandbox Code Playgroud)

在其他类我有一个下载图像的扩展方法Async:

   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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我也尝试使用像Haneke这样的缓存库,但问题是存在而不是改变.

请帮我理解错误

谢谢

Ben*_*ler 9

设置图像后,您应该调用 self.layoutSubviews()

编辑:更正setNeedsLayoutlayoutSubviews


Rob*_*Rob 8

问题是,.Subtitle的演绎UITableViewCell将尽快布局对小区的cellForRowAtIndexPath收益(覆盖您尝试将frame图像视图).因此,如果您异步检索图像,则单元格将被重新布局,就像没有要显示的图像一样(因为您没有将图像视图的image属性初始化为任何内容),并且当您imageView稍后异步更新时,将以某种方式布置单元格,使您无法看到下载的图像.

这里有几个解决方案:

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

    extension UIImageView {
        func downloadFrom(link link:String?, contentMode mode: UIViewContentMode) {
            contentMode = mode
            image = UIImage(named: "default")
            if link != nil, let url = NSURL(string: link!) {
                NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
                    guard let data = data where error == nil else {
                        print("\nerror on download \(error)")
                        return
                    }
                    if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode != 200 {
                        print("statusCode != 200; \(httpResponse.statusCode)")
                        return
                    }
                    dispatch_async(dispatch_get_main_queue()) {
                        print("\ndownload completed \(url.lastPathComponent!)")
                        self.image = UIImage(data: data)
                    }
                    }.resume()
            } else {
                self.image = UIImage(named: "default")
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这确保了单元格将被布置为存在图像,无论如何,因此图像视图的异步更新将起作用(类似于:见下文).

  2. 您可以创建自己的单元格原型,而不是使用动态布局的.Subtitle再现UITableViewCell,您可以使用固定大小的图像视图进行适当的布局.这样,如果没有立即可用的图像,它将不会重新格式化单元格,就像没有可用的图像一样.这使您可以使用autolayout完全控制单元格的格式.

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

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

  1. 如果向下滚动然后向上滚动,则将从网络重新检索图像.您确实希望在再次检索之前缓存先前下载的图像.

    理想情况下,您的服务器响应标头配置正确,以便内置的NSURLCache将为您处理此问题,但您必须测试它.或者,您可以自己将图像缓存NSCache.

  2. 如果你快速向下滚动到第100行,你真的不希望可见的单元格在前面不再可见的99行的图像请求后面积压.你真的想取消滚动屏幕的单元格的请求.(或者dequeueCellForRowAtIndexPath,在重新使用单元格的地方使用,然后您可以编写代码来取消之前的请求.)

  3. 如上所述,您确实希望这样做,dequeueCellForRowAtIndexPath以便您不必不必要地实例化UITableViewCell对象.你应该重用它们.

就个人而言,我可能会建议您(a)使用dequeueCellForRowAtIndexPath,然后(b)将其与一个完善的UIImageViewCell类别(如AlamofireImage,SDWebImage,DFImageManagerKingfisher)结合使用.对先前请求进行必要的缓存和取消是一项非常重要的练习,使用其中一个UIImageView扩展将简化您的生活.如果您决定自己这样做,您可能还想查看这些扩展的一些代码,以便您可以了解如何正确执行此操作.

-

例如,使用AlamofireImage,您可以:

  1. 定义自定义表视图单元子类:

    class CustomCell : UITableViewCell {
        @IBOutlet weak var customImageView: UIImageView!
        @IBOutlet weak var customTitleLabel: UILabel!
        @IBOutlet weak var customSubtitleLabel: UILabel!
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 将单元格原型添加到表视图故事板中,指定(a)基类CustomCell; (b)故事板编号CustomCell; (c)将图像视图和两个标签添加到您的单元格原型中,将其连接@IBOutlets到您的CustomCell子类; (d)添加定义图像视图的位置/大小和两个标签所需的任何约束.

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

  3. cellForRowAtIndexPath可以做以下事情:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
    
        let record = dataSource[indexPath.row]
        cell.customTitleLabel.text = record.Title
        cell.customSubtitleLabel.text = record.SubTitle
        if let urlString = record.Icon {
            cell.customImageView.af_setImageWithURL(NSURL(string: urlString)!)
        }
    
        return cell
    }
    
    Run Code Online (Sandbox Code Playgroud)

这样,您不仅可以享受基本的异步图像更新,还可以享受图像缓存,可见图像的优先级排序,因为我们重新使用出列单元格,效率更高等等.并且通过使用带有约束的单元格原型和您的自定义表视图单元子类,一切都正确布局,使您无需手动调整frame代码.

无论UIImageView您使用哪种扩展程序,这个过程都大致相同,但目标是让您摆脱自己编写扩展程序的杂草.