UITableViewCell异步加载图像问题 - Swift

4
在我的应用中,我建立了自己的异步图像加载类。我传入一个对象,然后它会检查缓存(NSCache)是否有该图像,如果没有,则会检查文件系统是否已经保存了该图像。如果该图像还没有被保存,那么它将在后台下载图像(使用NSOperations)。目前为止,这个方法运行得很好,但是我在表视图加载图像时遇到了一些小问题。
首先,这是我使用的函数来设置表视图单元格,来自于tableView(tableView:, willDisplayCell:, forRowAtIndexPath:)
func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {

    // Configure cell
    if let show = dataSource.showFromIndexPath(indexPath) {

        ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
            if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
                self.indexPathsForFadedInImages.append(indexPath)

                if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
                    func fadeInImage() {
                        // Fade in image
                        fetchCell.backgroundImageView!.alpha = 0.0
                        fetchCell.backgroundImage = image
                        UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
                            fetchCell.backgroundImageView!.alpha = 1.0
                        })
                    }

                    if #available(iOS 9, *) {
                        if NSProcessInfo.processInfo().lowPowerModeEnabled {
                            fetchCell.backgroundImage = image
                        }
                        else {
                            fadeInImage()
                        }
                    }
                    else {
                        fadeInImage()
                    }
                }
                else {
                    // Issues are here
                }
            }
            else {
                // Set image
                cell.backgroundImage = image
            }
        })
...
}

在“// Issues are here”注释的位置,我遇到了多个问题。

到目前为止,我还没有找到另一种方法来验证图片确实属于“// Issues are here”所在的单元格。如果我在那里添加

cell.backgroundImage = image

那么它就可以解决有时图像不会显示在表格视图单元格上的问题。到目前为止,我发现唯一的原因是图像返回得比我返回表格视图单元格的速度更快,这就是为什么表格视图说在该索引路径处没有单元格的原因。

但是,如果我在那里添加那段代码,那么我就会遇到另一个问题!单元格将显示错误的图像,然后拖慢应用程序,图像将不断切换,甚至只停留在错误的图像上。

我已经检查过它在主线程上运行,图像下载和缓存都很好。它只是要做的是表格正在说在该索引路径处没有单元格,我尝试获取单元格的indexPath也返回nil。

这个问题的半个解决方案是在viewWillAppear/viewDidAppear中调用tableView.reloadData()。这将解决问题,但我失去了屏幕上表格视图单元格的动画效果。

编辑:

如果我将imageView传递到getImageForShow()并直接设置它,它将解决这个问题,但这是不太理想的代码设计。显然,imageView存在,单元格也存在,但由于某种原因,它并不总是起作用。

1个回答

8
视图重复使用单元格以节省内存,这可能会导致任何需要执行异步例程以显示单元格数据(如加载图像)的问题。如果单元格在异步操作完成时应该显示不同的数据,则应用程序可能会突然进入不一致的显示状态。 为了解决这个问题,我建议向您的单元格添加一个生成属性,并在异步操作完成时检查该属性:
protocol MyImageManager {
    static var sharedManager: MyImageManager { get }
    func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}

struct MyCellData {
    let url: String
}

class MyTableViewCell: UITableViewCell {

    // The generation will tell us which iteration of the cell we're working with
    var generation: Int = 0

    override func prepareForReuse() {
        super.prepareForReuse()
        // Increment the generation when the cell is recycled
        self.generation++
        self.data = nil
    }

    var data: MyCellData? {
        didSet {
            // Reset the display state
            self.imageView?.image = nil
            self.imageView?.alpha = 0
            if let data = self.data {
                // Remember what generation the cell is on
                var generation = self.generation
                // In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
                weak var wcell = self
                // Retrieve the image from the server (or from the local cache)
                MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
                    if let error = error {
                        println("There was a problem fetching the image")
                    } else if let cell = wcell, image = image where cell.generation == generation {
                        // Make sure that UI updates happen on main thread
                        dispatch_async(dispatch_get_main_queue(), { () -> Void in
                            // Only update the cell if the generation value matches what it was prior to fetching the image
                            cell.imageView?.image = image
                            cell.imageView?.alpha = 0
                            UIView.animateWithDuration(0.25, animations: { () -> Void in
                                cell.imageView?.alpha = 1
                            })
                        })
                    }
                })
            }
        }
    }
}

class MyTableViewController: UITableViewController {

    var rows: [MyCellData] = []

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
        cell.data = self.rows[indexPath.row]
        return cell
    }

}

还有一些注意事项:

  • 不要忘记在主线程上进行显示更新。在网络活动线程上更新可能会导致显示在看似随机的时间改变(或永远不会改变)。
  • 在执行异步操作时,请务必弱引用单元格(或任何其他UI元素),以防在异步操作完成之前销毁UI。

我做了一些不同的事情,但是生成变量起作用了。我将我的configureCell代码保留在原地,并在那里检查生成变量,从我所看到的情况来看,图像现在总是正确的。谢谢 :) - Maximilian Litteral
1
这个问题是,在滚动回到顶部时,图像没有正确设置,因为您总是在prepareForReuse中增加生成值。 - BalestraPatrick
你可以使用标签属性代替生成。 - Bogdan Razvan

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