我有 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")
}
答案 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
在这种情况下,由于并发,您可能会遇到以下情况:
为解决此问题,我们将注意力转向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一样从单元格本身获取indexPath
和tableView
,但请确保在之前调用异步函数完成该操作。 / p>