Ios 异步图像加载到UITableViewCell后滚动时,Swift图像会变为错误图像

Ios 异步图像加载到UITableViewCell后滚动时,Swift图像会变为错误图像,ios,swift,image,uitableview,asynchronous,Ios,Swift,Image,Uitableview,Asynchronous,我正在尝试在FriendsTableView(UITableView)单元格中异步加载图片。图像加载良好,但当我滚动表格时,图像会更改几次,错误的图像被分配到错误的单元格 我已经尝试了在StackOverflow中可以找到的所有方法,包括向原始文件添加一个标记,然后检查它,但都不起作用。我还在验证应该使用indexPath更新的单元格,并检查该单元格是否存在。所以我不知道为什么会这样 这是我的密码: func tableView(tableView: UITableView, cellF

我正在尝试在FriendsTableView(UITableView)单元格中异步加载图片。图像加载良好,但当我滚动表格时,图像会更改几次,错误的图像被分配到错误的单元格

我已经尝试了在StackOverflow中可以找到的所有方法,包括向原始文件添加一个标记,然后检查它,但都不起作用。我还在验证应该使用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
    }

这是因为UITableView重用单元格。以这种方式加载它们会导致异步请求在不同的时间返回,从而打乱顺序

我建议你使用一些图书馆,这将使你的生活更容易像翠鸟。它将为您下载和缓存图像。此外,您不必担心异步调用

您的代码与此类似:

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
    }

在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;
   }});
});

更新

有一些很棒的开源库用于图像缓存,比如和。我建议您尝试其中之一,而不是编写自己的实现

结束更新

因此,为了让它工作,您需要做几件事。首先让我们看看缓存代码

// Global variable or stored in a singleton / top level object (Ex: AppCoordinator, AppDelegate)
let imageCache = NSCache<NSString, UIImage>()

extension UIImageView {

    func downloadImage(from imgURL: String) -> URLSessionDataTask? {
        guard let url = URL(string: imgURL) else { return nil }

        // 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 nil
        }

        // download the image asynchronously
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let err = error {
                print(err)
                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()
        return task
    }
}
要在TableView或CollectionView单元格中使用此选项,您需要在
prepareforeuse
中将图像重置为nil,然后取消下载任务。(谢谢你指出这一点


请记住,即使您使用第三方库,您仍然希望在
prepareForReuse

中取消图像并取消任务。我所拥有的解决此问题的最佳方案是Swift 3或Swift 4 只需写这两行

  cell.videoImage.image = nil

 cell.thumbnailimage.setImageWith(imageurl!)

根据实施情况,可能有许多事情会导致这里的所有答案都不起作用(包括我的)。检查标记对我来说不起作用,检查缓存也不起作用,我有一个自定义的Photo类,其中包含完整的图像、缩略图和更多数据,因此我也必须注意这一点,而不仅仅是防止图像被不正确地重用。因为在下载完图像后,您可能会将图像分配给cell imageView,y您需要取消下载并在prepareForReuse()上重置所需的任何内容

例如,如果您正在使用类似SDWebImage的内容

  override func prepareForReuse() {
   super.prepareForReuse() 

   self.imageView.sd_cancelCurrentImageLoad()
   self.imageView = nil 
   //Stop or reset anything else that is needed here 

}
如果您已将imageview子类化并自行处理下载,请确保设置了在调用完成之前取消下载的方法,并在
prepareforuse()上调用取消

e、 g

您也可以从UIViewController中取消此操作。此操作本身或与某些答案结合使用将很可能解决此问题。

Swift 3

  DispatchQueue.main.async(execute: {() -> Void in

    if cell.tag == indexPath.row {
        var tmpImage = UIImage(data: imgData)
        thumbnailImageView.image = tmpImage
    }
})

我只需实现一个自定义的
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()
    }
}
让imageCache=NSCache()
类CustomImageView:UIImageView{
var imageUrlString:String?
func downloadImageFrom(带URL urlString:String){
imageUrlString=urlString
让url=url(字符串:urlString)
self.image=nil
如果让cachedImage=imageCache.object(forKey:urlString作为NSString){
self.image=cachedImage
返回
}
URLSession.shared.dataTask(带有:url!,completionHandler:{(数据,响应,错误)的
如果错误!=nil{
打印(错误!)
返回
}
DispatchQueue.main.async{
如果让image=UIImage(数据:data!){
setObject(image,forKey:NSString(string:urlString))
如果self.imageUrlString==urlString{
self.image=image
}
}
}
})1.简历()
}
}

它适合我。

如果目标是iOS 13或更高版本,您可以使用和。请参阅WWDC 2019视频

这样做的目的是让单元格跟踪“发布者”,并让
prepareforeuse

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
            }
        }
    }
}
  • 取消先前的图像请求
  • 将图像视图的
    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()
    }
}
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
    }
}
其中:

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()
    }
}
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
    }
}
类图像管理器{
静态let shared=ImageManager()
私有init(){}
私有let会话:URLSession={
let configuration=URLSessionConfiguration.default
configuration.requestCachePolicy=.returnCacheDataElseLoad
let session=URLSession(配置:配置)
返回会话
}()
枚举ImageManager错误:错误{
案例无效应答
}
@可丢弃结果
func imageTask(对于url:url,完成:@escaping(Result)->Void)->URLSessionTask{
让task=session.dataTask(with:url){data,response,中的错误
guard let data=其他数据{
DispatchQueue.main.async{completion(.failure(error!))}
返回
}
警卫
让httpResponse=响应为?HTTPURLResponse,

200..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()
 }
}

因为TableView重用单元格。请在单元格类中尝试以下代码:

class CustomViewCell: UITableViewCell {

@IBOutlet weak var catImageView: UIImageView!

private var task: URLSessionDataTask?

override func prepareForReuse() {
    super.prepareForReuse()
    task?.cancel()
    catImageView.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.catImageView.image = image
            }
        }
    }
    task?.resume()
   } 
 }

我在我的模型中创建了一个新的UIImage变量,并在创建新模型实例时从那里加载图像/占位符。它工作得非常好。

下载后在内存和磁盘上使用翠鸟缓存就是一个例子。 它取代了传统的UrlSession下载,避免了在向下滚动TableViewCell后重新下载UIImageView

class CustomViewCell: UITableViewCell { @IBOutlet weak var catImageView: UIImageView! private var task: URLSessionDataTask? override func prepareForReuse() { super.prepareForReuse() task?.cancel() catImageView.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.catImageView.image = image } } } task?.resume() } }
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))