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

时间:2019-12-06 05:20:35

标签: swift uitableview uiimage

我有 override func viewDidLoad() { super.viewDidLoad() refController.bounds = CGRect.init(x: 0.0, y: 40.0, width: refController.bounds.size.width, height: refController.bounds.size.height) refController.attributedTitle = NSAttributedString(string: "refreshing") WKWebView.scrollView.addSubview(refController) WKWebView.scrollView.delegate = self if contentController.userScripts.count > 0 { contentController.removeAllUserScripts() } } 列出了其中包含图片的社交媒体帖子。 一旦所有帖子的详细信息都已加载并且缓存的图像看起来不错,但是在加载时经常会显示错误的图片和错误的帖子。

几个月来,我一直在努力并重新回到这个问题上。我不认为这是一个加载问题,它几乎就像iOS将图像转储到任何旧单元格之前,直到找到正确的单元格为止,但是老实说我没有主意。

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

UITableView

这是我的 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() } } 的简化版:

UITableView

FeedItem类包括prepareForReuse(),如下所示:

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
}

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

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

5 个答案:

答案 0 :(得分:1)

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

cellForRowAt函数中,您调用:

cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)

滚动表格视图时,在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)

缓存图像时,您将始终按该顺序依次显示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

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

  • 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()

如您所见,即使不再显示与该图像相关联的内容,也要设置self.image = downloadedImage,因此您需要某种方法来检查是否仍然存在。由于您在loadImageUsingCacheWithUrlString的扩展名中定义了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

答案 1 :(得分:0)

您正在cellForRowAt函数中使用可重复使用的单元格,尽管该单元格一直在加载和卸载信息,但我们都知道下载图片时下载速度并不快,您需要下载图片cellForRowAt以外的任何功能。例如

如果您有一组网址

let arrayImages = ["url1", "url2", "url3"]
let downloadImages = [UIImage]()
var dispatchGroup = DispatchGroup()

UIImage的扩展名

import Foundation
import UIKit

extension UIImage {

    func downloaded(from url: URL, completion: ((UIImage,String) -> Void)?) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
                let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
                let data = data, error == nil,
                let image = UIImage(data: data)
                else { return }
            DispatchQueue.global().async() {
                completion?(image,url.absoluteString)
            }
            }.resume()
    }

    func downloaded(from link: String, completion: ((UIImage,String) -> Void)?) {
        guard let url = URL(string: link) else { return }
        downloaded(from: url, completion: completion)
    }
}

视图代码

override func viewWillAppear()
{
    super.viewWillAppear(true)
    for url in arrayImages 
    {
       dispatchGroup.enter()
       let imageDownloaded = UIImage()
       imageDownloaded.downloaded(from: url) { (image, urlImage2) in
                DispatchQueue.main.async {
                    self.downloadImages.append(image)
                    self.tableView.reloadData()
                    self.dispatchGroup.leave()
                }
            }     
    }
    dispatchGroup.notify(queue: .main) {
         tableView.reloadData()
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    
    let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
    cell.delegate = self
    if downloadImages.count > 0 {
        cell.postHeroImage.image = downloadImages[indexPath.row]
    }
    cell.postTitle.text = postArray [indexPath.row].postTitle
    cell.postDescription.text = postArray [indexPath.row].postBody

    return cell
}

如果您有任何疑问,请告诉我。我希望这可以为您提供帮助

答案 2 :(得分:0)

解决此问题的另一种方法是使用tableView(_:willDisplay:forRowAt:)从缓存中加载下载的图像,并使用tableView(_:didEndDisplaying:forRowAt:)从单元中删除图像

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    
    let cell = tableView.dequeueReusableCell(withIdentifier: "FeedItem", for: indexPath) as! FeedItem
    cell.delegate = self
    cell.postTitle.text = postArray [indexPath.row].postTitle
    cell.postDescription.text = postArray [indexPath.row].postBody

    return cell
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let feedCell = cell as! FeedItem 
    if downloadImages.count > 0 {
        cell.postHeroImage.image = downloadImages[indexPath.row]
    }
}

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let feedCell = cell as! FeedItem 
    cell.postHeroImage.image = nil
}

如果您使用的是CocoaPods,我强烈建议您使用 Kingfisher 处理项目的图像下载

答案 3 :(得分:0)

您需要根据图像URL本身下载并缓存图像,并在使用该URL加载表时使用它。

比方说,您有一个图像URL数组,使用此数组加载行数,下载的图像应映射到indexPath并进行缓存。稍后,您可以基于带有数组的indexPath使用它。

您面临的问题是下载的映像中的映射数据不同步行。随着TableViewCell双端队列的使用并重复使用该单元格。

答案 4 :(得分:0)

那是因为tableView重用了它的单元格。因此,一个单元格可以负责具有不同url的多个图像。

因此有一个简单的解决方案:

您应该传递IndexPath,而不是将引用传递给可重复使用的单元格。它是值类型,不会重复使用。

然后,当您从异步任务中获取了图像时,可以使用TableView函数向.cellForRow(at: indexPath)询问实际的单元格。

因此,摆脱这一行:

cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)

,然后将其替换为带有实际indexPath的函数,并可能引用tableView

观看this WWDC 2018 session了解更多信息。大约是UICollectionView,但与UITableView相同。

此外,您可以像this answer一样从单元格本身获取indexPathtableView,但请确保在之前调用异步函数完成该操作。 / p>