在UITableViewCell中异步加载图片:Swift

13

我用代码创建了一个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
}
}

另外一个类中我有一个异步下载图像的扩展方法:

   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这样的缓存库,但问题仍然存在,没有改变。

请帮助我理解错误之处,谢谢。


看起来你正在下载图片,但没有将其添加到单元格的图像视图中。另外,downloadFrom有两个参数,但在cellForRow中传递了三个参数。这必须会抛出错误。 - Vijay
亲爱的@Vijay:感谢您的评论,关于错误,您是正确的,我已经更改了它,但忘记更新代码,这不是错误。 您能否向我解释一下如何将图像添加到单元格中的更多信息? - Imran Sh
5个回答

11
在设置图像后,您应该调用self.layoutSubviews()
编辑:从setNeedsLayout更正为layoutSubviews

1
调用 layoutSubviews() 怎么样? - Bea Boxler
是的,它对我起作用了,请更新您的答案,这样我就可以将其标记为最终答案。 - Imran Sh
1
不应直接调用此方法。根据苹果文档,应该调用layoutIfNeeded()或setNeedsLayout()。https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews - Gil Sand
你好,我正在使用带有 NSTextAttachment 的单元格,但调用 setNeedsLayout 不起作用,但是上下滚动时 GIF 显示正常,你能帮忙吗? - famfamfam

11
问题在于,UITableViewCell.subtitle渲染会在cellForRowAtIndexPath返回时立即布局单元格(覆盖您对图像视图frame的设置尝试)。因此,如果您异步检索图像,则该单元格将被重新布局,就好像没有图像要显示一样(因为您未将图像视图的image属性初始化为任何内容),并且当您稍后异步地更新imageView时,单元格已经以一种方式进行布局,使您无法看到所下载的图像。

这里有几个解决方案:

  1. 您可以在没有URL时将download更新的图像设置为默认值,而不仅仅是在有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()
        }
    }
    

    这样可以确保单元格的布局不受图片是否存在的影响,并因此异步更新图像视图(某种程度上:见下文)。

    与使用动态布局的.subtitle重现UITableViewCell不同,您还可以创建自己的单元格原型,该原型具有适当的布局和图像视图的固定大小。这样,如果没有立即可用的图像,它不会重新格式化单元格,就好像没有可用的图像一样。使用autolayout使您完全控制单元格的格式设置。

    您还可以定义您的downloadFrom方法以接受第三个额外参数,这是在下载完成时将调用的一个闭包。然后,您可以在该闭包内执行reloadRowsAtIndexPaths。但是,这假设您修复了此代码以缓存已下载的图像(例如在NSCache中),以便在再次下载之前可以检查是否具有缓存的图像。

    尽管如上所述,这种基本模式存在一些问题:

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

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

    2. 如果快速向下滚动到第100行,则不希望可见单元格被堵住,等待第1至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!
    }
    
    将一个单元格原型添加到你的表视图Storyboard中,指定以下内容:(a) 一个基类为CustomCell的单元格;(b) 一个故事板ID为CustomCell;(c) 在单元格原型上添加图像视图和两个标签,并将 @IBOutlets 钩连到你的CustomCell子类中;(d) 添加必要的约束条件来定义图像视图和两个标签的位置/大小。你可以使用自动布局约束来定义图像视图的尺寸。

    然后,你的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
    }
    

    这样一来,您不仅可以享受基本的异步图像更新,还可以使用图像缓存、优先显示可见图像(因为我们正在重用出列的单元格,所以更有效率等)。通过使用带有约束条件的单元格原型和您自定义的表视图单元格子类,所有内容都会被正确地布局,节省了手动在代码中调整frame的时间。

    无论您使用哪个UIImageView扩展,过程大致相同,目的是让您摆脱自己编写扩展的困境。


1
顺便提一下,在上面的代码示例中,我保留了您的 TitleSubTitleIcon 属性名称,但您真的应该遵循 Cocoa 命名约定,以小写字母开头的属性名称。 - Rob
非常非常有用,我很幸运能够拥有您的经验。c.layoutSubviews()解决了我的问题,我从您的帖子中学到了很多东西,但是您的第三个解决方案是错误的,因为它使表视图处于递归状态更新自身(在提问之前我尝试过)。 - Imran Sh
关于缓存,你说的绝对是对的,我在将像Haneke和Alamofire这样的框架添加到我的Xcode中时遇到了很大的问题,我不知道该如何解决!!!http://stackoverflow.com/questions/33241713/swift-add-haneke-framework-to-xcode-7-0-1-not-work - Imran Sh
1
第三种方法,调用reloadCells...是完全正常的,只要cellForRow...第一次检索到所有需要的内容,并且第二次只布局所有内容。实际上,有时这是唯一有效的方法,并且它提供了轻松控制动画的能力。但是你是对的,如果你做得不正确,你可能会陷入无限递归的境地。 - Rob

3

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


2

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


1
这不是真的。.Subtitle 单元格也支持图像视图。问题在于,当 cellForRowAtIndexPath 返回时是否存在图像(他将其留空为 nil)会影响单元格的布局,并且异步提供 image 并不能使单元格在下载图像时正确地重新格式化。最简单的解决方案是提供一个占位符图像(这样单元格就会正确布局),异步图像更新基本上也能正常工作。 - Rob
亲爱的Darko,非常感谢您的回答。 我认为@Rob是正确的,我找到了解决方案。 - Imran Sh

2

推荐使用SDWebImages库,这是链接

它可以异步下载并缓存图片,并且非常容易集成到项目中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接