将 url 图片设置为 UITableViewCell 时出现问题。 我创建了
CancelableImageView
,它是自定义的 UIImageView
来在设置新图像时取消图像下载和设置任务。
通过使用这种方法,“错误图像”问题似乎已经解决,但 UITableView 仍然发生这种情况
加载
UITableView
后,一切看起来都很棒,但是在第一个单元格完成加载图像并更改图像后,第四个单元格的图像也更改为第一个单元格的图像。滚动后第四个单元格的图像更改为正确的图像。
这是我的 UIImage 图像加载扩展(包含图像缓存)
/// Singleton for Image Caching
class ImageCacheManager {
/// Storage to save cached UIImage
private let cache = Cache<String, UIImage>()
/// singleton instance
static let shared = ImageCacheManager()
/**
Get image from cache data for url String
- Parameter key: String url of UIImage
- Returns: Retrun cached image for url. Retrun nil when cached image is not exist
*/
func loadCachedData(for key: String) -> UIImage? {
let itemURL = NSString(string: key)
return cache.value(forKey: key)
}
/**
Save UIImage to cache data
- Parameters:
- image: UIImage to save in cache data
- key: String url of UIImage
*/
func saveCacheData(of image: UIImage, for key: String) {
let itemURL = NSString(string: key)
cache.insertValue(image, forKey: key)
}
}
extension UIImageView {
/**
Set image to UIImageView with Cached Image data or data from URL
- Parameters:
- urlString: String url of image
- forceOption: Skip getting image from cache data and force to get image from url when true. default false
*/
func loadImage(_ urlString: String?, forceOption: Bool = false) -> UIImage? {
guard let imagePath = urlString else { return nil }
// Cached Image is available
if let cachedImage = ImageCacheManager.shared.loadCachedData(for: imagePath), forceOption == false {
return cachedImage
// No Image Cached
} else {
guard let imageURL = URL(string: imagePath) else { return nil }
guard let imageData = try? Data(contentsOf: imageURL) else { return nil }
guard let newImage = UIImage(data: imageData) else { return nil }
ImageCacheManager.shared.saveCacheData(of: newImage, for: imagePath)
return newImage
}
}
func setImage(_ urlString: String?, forceOption: Bool = false) {
DispatchQueue.global().async {
guard let image = self.loadImage(urlString, forceOption: forceOption) else { return }
DispatchQueue.main.async {
self.image = image
}
}
}
}
这是自定义 UIImage,在设置新图像时取消图像加载任务:
/// UIImageView with async image loading functions
class CancelableImageView: UIImageView {
/// Cocurrent image loading work item
private var imageLoadingWorkItem: DispatchWorkItem?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init() {
super.init(frame: CGRect.zero)
}
}
// MARK: Concurrent image loading method
extension CancelableImageView {
/**
Cancel current image loading task and Set image to UIImageView with Cached Image data or data from URL
- Parameters:
- urlString: String url of image
- forceOption: Skip getting image from cache data and force to get image from url when true. default false
*/
func setNewImage(_ urlString: String?, forceOption: Bool = false) {
self.cancelLoadingImage()
self.imageLoadingWorkItem = DispatchWorkItem { super.setImage(urlString, forceOption: forceOption) }
self.imageLoadingWorkItem?.perform()
}
/// Cancel current image loading work
func cancelLoadingImage() {
DispatchQueue.global().async {
self.imageLoadingWorkItem?.cancel()
}
}
}
这是我的该视图的 UITableViewCell:
class ChartTableViewCell: UITableViewCell {
lazy var posterImageView = CancelableImageView().then {
$0.contentMode = .scaleAspectFit
$0.image = UIImage(named: "img_placeholder")
}
...
override func prepareForReuse() {
setPosterPlaceholderImage()
super.prepareForReuse()
}
}
extension ChartTableViewCell {
/// Set poster imageView placeholder Image
private func setPosterPlaceholderImage() {
self.posterImageView.image = UIImage(named: "img_placeholder")
}
// MARK: Set Data
func setData(rank: Int, movie: MovieFront) {
rankLabel.text = "\(rank+1)"
titleLabel.text = movie.title
genreLabel.text = movie.genre
releaseDateLabel.text = movie.releaseDate
starRating.rating = movie.ratingScore/2
ratingCountLabel.text = "(\(movie.ratingCount))"
if let imagePath = movie.posterPath {
posterImageView.setNewImage(APIService.configureUrlString(imagePath: imagePath))
}
}
}
这是 UITableViewDataSource (cellForRowAt):
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: identifiers.chart_table_cell) as? ChartTableViewCell else {
return UITableViewCell()
}
cell.setData(rank: indexPath.row, movie: viewModel.movieListData.value[indexPath.row])
return cell
}
一些观察:
在
setImage
中,异步self.image = image
是有问题的,因为您不知道单元格(以及图像视图)是否已被表/集合视图中的另一行/项目重用。
一般解决方案(在
UIImageView
扩展中)是为 URL 提供一个“关联值”(例如 objc_setAssociatedObject(_:_:_:_:)
等),并确保此特定图像视图的 URL 仍然与您的 URL 相同。只是在更新 image
属性之前异步获取。如果您确实想使用子类方法,只需将其存储在子类的某个属性中(并完全丢失 UIImageView
扩展名)。优雅的解决方案是不使用任何子类方法图像视图子类,但将其放在 UIImageView
扩展名中,并使用“关联值”作为您需要跟踪特定图像视图实例的任何其他值。
您正在取消调度工作项目,但实际上并未取消请求。 (取消是“合作的”,而不是“抢先的”。)更糟糕的是,您甚至没有使用可取消的网络请求。
我建议不要使用调度工作项和
Data(contentsOf:)
,而是使用 URLSession
并保存 URLSessionTask
。那是可以取消的。并且,我们将再次使用“关联值”来保存旧的 URLSessionTask
,以防您需要取消它。请参阅https://stackoverflow.com/a/45183939/1271826。
坦率地说,以上是关于修复图像视图的异步图像检索功能中的问题的一般观察,但您可能还会遇到另一个问题。具体来说,上面修复了因滚动单元格进出视图而产生的问题,但您的动画 gif 显示了另一个问题,其中更新第一个单元格的图像似乎正在更新第四个单元格的图像。如果更新某些静态/全局属性(或类似的属性),这种情况很常见,而我实际上在您与我们共享的代码中没有看到这种情况。我认为我们可能需要 MRE 来帮助进一步诊断。
但是,坦率地说,我可能鼓励您考虑建立异步图像检索和缓存库,例如 KingFisher、SDWebImage、AlamofireImage。所有这些都非常复杂,因此谨慎的做法是不要“重新发明轮子”。