我正在尝试在FriendsTableView(UITableView)单元格中异步加载图片。图像加载正常,但是当我滚动表格时,图像会改变几次,错误的图像会分配给错误的单元格。
我已经尝试了我在StackOverflow中可以找到的所有方法,包括向raw添加标签然后检查它但是没有用。我还要验证应该使用indexPath更新的单元格并检查单元格是否存在。所以我不知道为什么会这样。
这是我的代码:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
getDataFromUrl(avatar_url) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
let thisCell = tableView.cellForRowAtIndexPath(indexPath)
if (thisCell) != nil {
let updateCell = thisCell as! FriendTableViewCell
updateCell.friendAvatar.image = UIImage(data: data)
}
}
}
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
答案 0 :(得分:18)
on cellForRowAtIndexPath:
1)为自定义单元格指定索引值。例如,
cell.tag = indexPath.row
2)在主线程上,在分配图像之前,通过将图像与标记匹配来检查图像是否属于相应的单元格。
dispatch_async(dispatch_get_main_queue(), ^{
if(cell.tag == indexPath.row) {
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
}});
});
答案 1 :(得分:7)
这是因为UITableView重用了单元格。以这种方式加载它们会导致异步请求在不同时间返回并弄乱订单。
我建议您使用一些可以让您的生活更轻松的图书馆,就像翠鸟一样。它将为您下载和缓存图像。你也不必担心异步电话。
https://github.com/onevcat/Kingfisher
你的代码看起来像这样:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
var avatar_url: NSURL
let friend = sortedFriends[indexPath.row]
//Style the cell image to be round
cell.friendAvatar.layer.cornerRadius = 36
cell.friendAvatar.layer.masksToBounds = true
//Load friend photo asyncronisly
avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
if avatar_url != "" {
cell.friendAvatar.kf_setImageWithURL(avatar_url)
}
cell.friendNameLabel.text = friend["friend_name"].string
cell.friendHealthPoints.text = String(friend["friend_health_points"])
return cell
}
答案 2 :(得分:3)
<强>更新强>
以下代码确实有效,但后来我转而使用KingFisher这非常简单。
END UPDATE
我不想使用第三方库,例如Alamofire和cell.tag对我不起作用。将初始图像设置为nil对我来说很有用。我也缓存了图像。这是我为UIImageView编写的扩展名:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
您可以在cellForRowAt
中调用此方法cell.postImage.downloadImage(from: self.posts[indexPath.row].pathToImage)
答案 3 :(得分:1)
根据实施的不同,可能会有很多因素导致这里的所有答案都无效(包括我的)。检查标签对我来说不起作用,也没有检查缓存,我有一个自定义的Photo类,它带有完整的图像,缩略图和更多的数据,所以我也要照顾它,而不仅仅是防止图像被不正确地重复使用。由于您可能会在完成下载后将图像分配给单元imageView,因此您需要取消下载并重置prepareForReuse所需的任何内容()
示例,如果你正在使用像SDWebImage这样的东西
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.sd_cancelCurrentImageLoad()
self.imageView = nil
//Stop or reset anything else that is needed here
}
如果您已经将图像视图子类化并自行处理下载,请确保在调用完成之前设置取消下载的方法并在prepareForReuse()
上调用取消
e.g。
imageView.cancelDownload()
你也可以从UIViewController中取消它。这个问题本身或与一些答案相结合,很可能会解决这个问题。
答案 4 :(得分:1)
雨燕3
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
var tmpImage = UIImage(data: imgData)
thumbnailImageView.image = tmpImage
}
})
答案 5 :(得分:1)
我仅通过实现自定义UIImage
类就解决了问题,并且我做了一个String
条件,如下所示:
let imageCache = NSCache<NSString, UIImage>()
class CustomImageView: UIImageView {
var imageUrlString: String?
func downloadImageFrom(withUrl urlString : String) {
imageUrlString = urlString
let url = URL(string: urlString)
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
if let image = UIImage(data: data!) {
imageCache.setObject(image, forKey: NSString(string: urlString))
if self.imageUrlString == urlString {
self.image = image
}
}
}
}).resume()
}
}
对我有用。
答案 6 :(得分:1)
TableView重用单元格。试试这个:
import UIKit
class CustomViewCell: UITableViewCell {
@IBOutlet weak var imageView: UIImageView!
private var task: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
imageView.image = nil
}
func configureWith(url string: String) {
guard let url = URL(string: string) else { return }
task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
task?.resume()
}
}
答案 7 :(得分:1)
因为TableView重用了单元格。在您的单元格中尝试以下代码:
BehaviorSubject
答案 8 :(得分:0)
我遇到的最佳解决方案是Swift 3或Swift 4 只需写下这两行
cell.videoImage.image = nil
cell.thumbnailimage.setImageWith(imageurl!)
答案 9 :(得分:0)
如果定位到iOS 13或更高版本,则可以使用Combine和dataTaskPublisher(for:)
。这个想法是让单元格跟踪“发布者”,并拥有prepareForReuse
:
image
属性设置为nil
(或占位符);然后例如:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let url = ...
cell.setImage(to: url)
return cell
}
}
class CustomCell: UITableViewCell {
@IBOutlet weak var customImageView: UIImageView!
private var subscriber: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
subscriber?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
subscriber = ImageManager.shared.imagePublisher(for: url, errorImage: UIImage(systemName: "xmark.octagon"))
.assign(to: \.customImageView.image, on: self)
}
}
位置:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
func imagePublisher(for url: URL, errorImage: UIImage? = nil) -> AnyPublisher<UIImage?, Never> {
session.dataTaskPublisher(for: url)
.tryMap { data, response in
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
throw ImageManagerError.invalidResponse
}
return image
}
.replaceError(with: errorImage)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
如果定位的是较早的iOS版本,而不是使用Combine,则可以使用URLSession
,并取消prepareForReuse
中的先前请求:
class CustomCell: UITableViewCell {
@IBOutlet weak var customImageView: UIImageView!
private weak var task: URLSessionTask?
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
customImageView?.image = nil
}
func setImage(to url: URL) {
task = ImageManager.shared.imageTask(for: url) { result in
switch result {
case .failure(let error): print(error)
case .success(let image): self.customImageView.image = image
}
}
}
}
位置:
class ImageManager {
static let shared = ImageManager()
private init() { }
private let session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: configuration)
return session
}()
enum ImageManagerError: Error {
case invalidResponse
}
@discardableResult
func imageTask(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask {
let task = session.dataTask(with: url) { data, response, error in
guard let data = data else {
DispatchQueue.main.async { completion(.failure(error!)) }
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode,
let image = UIImage(data: data)
else {
DispatchQueue.main.async { completion(.failure(ImageManagerError.invalidResponse)) }
return
}
DispatchQueue.main.async { completion(.success(image)) }
}
task.resume()
return task
}
}
答案 10 :(得分:0)
我在模型中创建了一个新的UIImage变量,并在创建新模型实例时从那里加载了图像/占位符。效果很好。
答案 11 :(得分:0)
这是一个示例,在下载后在内存和磁盘上使用Kingfisher缓存。 它取代了传统的UrlSession下载,并向下滚动TableViewCell后避免重新下载UIImageView
https://gist.github.com/andreconghau/4c3b04205195f452800d2892e91a079a
示例输出
sucess
Image Size:
(460.0, 460.0)
Cache:
disk
Source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))
Original source:
network(Kingfisher.ImageResource(cacheKey: "https://avatars0.githubusercontent.com/u/5936?v=4", downloadURL: https://avatars0.githubusercontent.com/u/5936?v=4))