UITableViewCell 在图像加载时显示错误的图像
UITableViewCell shows the wrong image while images load
我有 UITableView
,其中列出了社交媒体 post,其中包含图片。
一旦加载了所有 post 详细信息并缓存了图像,它看起来很棒,但在加载它时 经常 显示带有错误 post 的错误图像。
几个月来我一直在努力并回到这个问题上。我不认为这是一个加载问题,它几乎看起来像 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()
}
}
这是我的 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
}
FeedItem Class 包含 prepareForReuse() 并且如下所示:
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
self.postHeroImage.image = UIImage(named: "loading")
}
编辑:这是我从 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()
}
}
你在你的cellForRowAt
函数中使用了一个可重复使用的cells,虽然cells一直在加载和卸载信息,但我们都知道在下载图片时,下载速度不快,你需要下载您的图像在除 cellForRowAt
之外的任何函数中。例如
如果你有一个 url 数组
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
}
如果您有任何疑问,请告诉我。希望对您有所帮助
另一种解决此问题的方法是使用 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 来处理您项目的图像下载
您需要根据图像 URL 本身下载并缓存图像,并在使用 url 加载 table 时使用它。
假设您有一个图像数组 URL 使用此数组加载行数,下载图像应映射到 indexPath 并缓存。稍后您可以根据带数组的 indexPath 使用它。
您遇到的问题是下载图像中的行与映射数据不同步。作为 TableViewCell 双端队列并重新使用单元格。
UITableView
在显示项目集合时仅使用少量单元格(~屏幕上可见单元格的最大数量),因此您拥有的项目多于单元格。这是因为 table 视图重用机制,这意味着相同的 UITableViewCell
实例将用于显示不同的项目。您遇到图像问题的原因是因为您没有正确处理细胞重用。
在您调用的 cellForRowAt
函数中:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
当您滚动 table 视图时,在 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
,因此您需要某种方法来检查情况是否仍然如此。由于您在 UIImageView
的扩展中定义了 loadImageUsingCacheWithUrlString
,因此您没有太多上下文可以知道是否应该显示该图像。相反,我建议将该函数移动到 UIImage
的扩展,这将 return 该图像在完成处理程序中,然后从您的单元格内部调用该函数。它看起来像:
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
那是因为 tableView
重复使用了它的单元格。因此,一个细胞可能负责具有不同 url
s.
的多个图像
所以有一个简单的解决方案:
您不应传递对可重用单元格的引用,而应传递 IndexPath
。它是值类型,不会重用。
然后当您从异步任务中获取图像后,您可以使用 .cellForRow(at: indexPath)
函数向 TableView
询问实际单元格。
所以,去掉这一行:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
并将其替换为一个函数,该函数采用实际的 indexPath 并且可能是对 tableView
.
的引用
观看 this WWDC 2018 session 了解更多信息。大约 UICollectionView
但与 UITableView
.
相同
您也可以像 一样从单元格本身获取 indexPath
和 tableView
,但请确保在 之前 调用异步函数。
我有 UITableView
,其中列出了社交媒体 post,其中包含图片。
一旦加载了所有 post 详细信息并缓存了图像,它看起来很棒,但在加载它时 经常 显示带有错误 post 的错误图像。
几个月来我一直在努力并回到这个问题上。我不认为这是一个加载问题,它几乎看起来像 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()
}
}
这是我的 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
}
FeedItem Class 包含 prepareForReuse() 并且如下所示:
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
self.postHeroImage.image = UIImage(named: "loading")
}
编辑:这是我从 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()
}
}
你在你的cellForRowAt
函数中使用了一个可重复使用的cells,虽然cells一直在加载和卸载信息,但我们都知道在下载图片时,下载速度不快,你需要下载您的图像在除 cellForRowAt
之外的任何函数中。例如
如果你有一个 url 数组
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
}
如果您有任何疑问,请告诉我。希望对您有所帮助
另一种解决此问题的方法是使用 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 来处理您项目的图像下载
您需要根据图像 URL 本身下载并缓存图像,并在使用 url 加载 table 时使用它。
假设您有一个图像数组 URL 使用此数组加载行数,下载图像应映射到 indexPath 并缓存。稍后您可以根据带数组的 indexPath 使用它。
您遇到的问题是下载图像中的行与映射数据不同步。作为 TableViewCell 双端队列并重新使用单元格。
UITableView
在显示项目集合时仅使用少量单元格(~屏幕上可见单元格的最大数量),因此您拥有的项目多于单元格。这是因为 table 视图重用机制,这意味着相同的 UITableViewCell
实例将用于显示不同的项目。您遇到图像问题的原因是因为您没有正确处理细胞重用。
在您调用的 cellForRowAt
函数中:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
当您滚动 table 视图时,在 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
,因此您需要某种方法来检查情况是否仍然如此。由于您在 UIImageView
的扩展中定义了 loadImageUsingCacheWithUrlString
,因此您没有太多上下文可以知道是否应该显示该图像。相反,我建议将该函数移动到 UIImage
的扩展,这将 return 该图像在完成处理程序中,然后从您的单元格内部调用该函数。它看起来像:
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
那是因为 tableView
重复使用了它的单元格。因此,一个细胞可能负责具有不同 url
s.
所以有一个简单的解决方案:
您不应传递对可重用单元格的引用,而应传递 IndexPath
。它是值类型,不会重用。
然后当您从异步任务中获取图像后,您可以使用 .cellForRow(at: indexPath)
函数向 TableView
询问实际单元格。
所以,去掉这一行:
cell.postHeroImage.loadImageUsingCacheWithUrlString(postImageURL)
并将其替换为一个函数,该函数采用实际的 indexPath 并且可能是对 tableView
.
观看 this WWDC 2018 session 了解更多信息。大约 UICollectionView
但与 UITableView
.
您也可以像 indexPath
和 tableView
,但请确保在 之前 调用异步函数。