在将异步图像加载到UITableViewCell后滚动时,Swift图像会更改为错误的图像

时间:2016-03-12 14:26:07

标签: ios swift image uitableview asynchronous

我正在尝试在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
    }

12 个答案:

答案 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)

Source1 Source2

答案 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或更高版本,则可以使用CombinedataTaskPublisher(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))