加载图像时 UITableViewCell 显示错误的图像

Bre*_*ski 1 uitableview uiimage swift

UITableView列出了其中包含图像的社交媒体帖子。一旦加载了所有帖子详细信息并缓存了图像,它看起来不错,但在加载时,它通常会显示带有错误帖子的错误图像。

几个月来我一直在努力并回到这个问题。我不认为这是一个加载问题,它几乎看起来像 iOS 将图像转储到任何旧单元格,直到找到正确的单元格,但老实说我没有想法。

这是我的图像扩展,它也负责缓存:

    let imageCache = NSCache<NSString, AnyObject>()

    extension UIImageView {

    func loadImageUsingCacheWithUrlString(_ urlString: String) {

        self.image = UIImage(named: "loading")

        if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
            self.image = cachedImage
            return
        }

        //No cache, so create new one and set image
        let url = URL(string: urlString)
        URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in

            if let error = error {
                print(error)
                return
            }

            DispatchQueue.main.async(execute: {

                if let downloadedImage = UIImage(data: data!) {
                    imageCache.setObject(downloadedImage, forKey: urlString as NSString)

                    self.image = downloadedImage
                }
            })

        }).resume()
    }

}
Run Code Online (Sandbox Code Playgroud)

这是我的缩短版本UITableView

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let postImageIndex = postArray [indexPath.row]
    let postImageURL = postImageIndex.postImageURL

    let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
    cell.delegate = self

    cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
    cell.postTitle.text = postArray [indexPath.row].postTitle
    cell.postDescription.text = postArray [indexPath.row].postBody

    return cell
}
Run Code Online (Sandbox Code Playgroud)

FeedItem 类包括 prepareForReuse() 并且看起来像这样:

override func prepareForReuse() {
    super.prepareForReuse()
    self.delegate = nil
    self.postHeroImage.image = UIImage(named: "loading")
}
Run Code Online (Sandbox Code Playgroud)

编辑:这是我从 Firebase 检索数据的方法:

func retrievePosts () {

    let postDB = Database.database().reference().child("MyPosts")

    postDB.observe(.childAdded) { (snapshot) in

        let snapshotValue =  snapshot.value as! Dictionary <String,AnyObject>

        let postID = snapshotValue ["ID"]!
        let postTitle = snapshotValue ["Title"]!
        let postBody = snapshotValue ["Description"]!
        let postImageURL = snapshotValue ["TitleImage"]!

        let post = Post()

        post.postTitle = postTitle as! String
        post.postBody =  postBody as! String
        post.postImageURL = postImageURL as! String

        self.configureTableView()
    }
}
Run Code Online (Sandbox Code Playgroud)

Rey*_*lar 5

UITableView显示项目集合时仅使用少数单元格(~屏幕上可见单元格的最大数量),因此您将拥有比单元格更多的项目。这是因为 table view 重用机制,这意味着相同的UITableViewCell实例将用于显示不同的项目。图像出现问题的原因是您没有正确处理单元重用。

cellForRowAt你调用的函数中:

cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
Run Code Online (Sandbox Code Playgroud)

当您滚动表格视图时,cellForRowAt将在同一单元格的不同调用中调用此函数,但(最有可能)显示不同项目的内容(因为单元格重用)。

假设 X 是您正在重用的单元格,那么这些是将被调用的大致函数:

1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)

// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
Run Code Online (Sandbox Code Playgroud)

缓存图像后,您将始终按该顺序有 1、2、3 和 4,这就是为什么在这种情况下您看不到任何问题的原因。但是,下载图像并将其设置为图像视图的代码在单独的线程中运行,因此不再保证该顺序。而不是只有上面的四个步骤,你将有类似的东西:

1. X.prepareForReuse()
// inside cellForRowAt
2. X.postHeroImage.loadImageUsingCacheWithUrlString(imageA)
// after download finishes 
2.1 X.imageView.image = downloadedImage

// at this point the cell is configured for displaying the content for imageA
// and later you reuse it for displaying the content of imageB
3. X.prepareForReuse()
// inside cellForRowAt
4. X.postHeroImage.loadImageUsingCacheWithUrlString(imageB)
4.1 X.imageView.image = downloadedImage
Run Code Online (Sandbox Code Playgroud)

在这种情况下,由于并发,您可能会遇到以下情况:

  • 1, 2, 2.1, 3, 4, 4.1: 一切正常显示(如果你缓慢滚动会发生这种情况)
  • 1, 2, 3, 2.1, 4, 4.1:在这种情况下,在调用重新使用单元格完成后第一张图像完成下载,因此旧图像将(错误地)显示一小段时间而新图像下载,然后替换。
  • 1, 2, 3, 4, 2.1, 4.1:与上述情况类似。
  • 1, 2, 3, 4, 4.1, 2.1:在这种情况下,旧图像在新图像之后完成下载(无法保证下载完成的顺序与它们开始的顺序相同),因此您最终会得到错误的图像。这是最坏的情况。

为了解决这个问题,让我们把注意力转向loadImageUsingCacheWithUrlString函数内部有问题的一段代码:

let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
    DispatchQueue.main.async(execute: {
        if let downloadedImage = UIImage(data: data!) {
            imageCache.setObject(downloadedImage, forKey: urlString as NSString)
            // this is the line corresponding to 2.1 and 4.1 above
            self.image = downloadedImage
        }
    })

}).resume()
Run Code Online (Sandbox Code Playgroud)

如您所见,self.image = downloadedImage即使不再显示与该图像关联的内容,您也正在设置,因此您需要某种方法来检查是否仍然如此。由于您loadImageUsingCacheWithUrlString在 for 的扩展中定义UIImageView,因此您没有太多上下文可以知道是否应该显示图像。取而代之的是,我建议将该函数移动到UIImage将在完成处理程序中返回该图像的扩展,然后从您的单元格内部调用该函数。它看起来像:

extension UIImage {
    static func loadImageUsingCacheWithUrlString(_ urlString: String, completion: @escaping (UIImage) -> Void) {
        if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
            completion(cachedImage)
        }

        //No cache, so create new one and set image
        let url = URL(string: urlString)
        URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
            if let error = error {
                print(error)
                return
            }

            DispatchQueue.main.async(execute: {
                if let downloadedImage = UIImage(data: data!) {
                    imageCache.setObject(downloadedImage, forKey: urlString as NSString)
                    completion(downloadedImage)
                }
            })

        }).resume()
    }
}

class FeedItem: UITableViewCell {
    // some other definitions here...
    var postImageURL: String? {
        didSet {
            if let url = postImageURL {
                self.image = UIImage(named: "loading")

                UIImage.loadImageUsingCacheWithUrlString(url) { image in
                    // set the image only when we are still displaying the content for the image we finished downloading 
                    if url == postImageURL {
                        self.imageView.image = image
                    }
                }
            }
            else {
                self.imageView.image = nil
            }
        }
    }
}

// inside cellForRowAt
cell.postImageURL = postImageURL
Run Code Online (Sandbox Code Playgroud)