图片下载和缓存问题

Image downloading and caching issue

我正在从服务器下载图片并在 collectionView 中显示。我正在缓存图像,以便用户获得快速的服务器响应并且 UI 中没有故障。在图片未下载之前,我也添加了占位符图片。

但在我的输出中,图像正在其他单元格中复制并且图像未正确缓存在 NSCache 中..

下面是代码

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    @IBOutlet weak var colView: UICollectionView!

    var imageCache = NSCache<NSString, UIImage>()
    var arrURLs = [
        "https://homepages.cae.wisc.edu/~ece533/images/airplane.png",
        "https://homepages.cae.wisc.edu/~ece533/images/arctichare.png",
        "https://homepages.cae.wisc.edu/~ece533/images/baboon.png",
        "https://homepages.cae.wisc.edu/~ece533/images/barbara.png",
        "https://homepages.cae.wisc.edu/~ece533/images/boat.png",
        "https://homepages.cae.wisc.edu/~ece533/images/cat.png",
        "https://homepages.cae.wisc.edu/~ece533/images/fruits.png",
        "https://homepages.cae.wisc.edu/~ece533/images/frymire.png",
        "https://homepages.cae.wisc.edu/~ece533/images/girl.png",
        "https://homepages.cae.wisc.edu/~ece533/images/goldhill.png",
        "https://homepages.cae.wisc.edu/~ece533/images/lena.png",
        "https://homepages.cae.wisc.edu/~ece533/images/monarch.png",
        "https://homepages.cae.wisc.edu/~ece533/images/mountain.png",
        "https://homepages.cae.wisc.edu/~ece533/images/peppers.png",
        "https://homepages.cae.wisc.edu/~ece533/images/pool.png",
        "https://homepages.cae.wisc.edu/~ece533/images/sails.png",
        "https://homepages.cae.wisc.edu/~ece533/images/serrano.png",
        "https://homepages.cae.wisc.edu/~ece533/images/tulips.png",
        "https://homepages.cae.wisc.edu/~ece533/images/watch.png",
        "https://homepages.cae.wisc.edu/~ece533/images/zelda.png"
    ]


func downloadImage(url: URL, imageView: UIImageView, placeholder : UIImage) {

    imageView.image = placeholder // Set default placeholder..

    // Image is set if cache is available
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        // Reset the image to placeholder as the URLSession fetches the new image
        imageView.image = placeholder
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                // You should be giving an option to retry the image here
                imageView.image = placeholder
                return
            }

            if let respo  = response as? HTTPURLResponse {

                print("Status Code : ", respo.statusCode)

                if let imageData = data, let image = UIImage(data: imageData) {
                    self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
                    // Update the imageview with new data
                    DispatchQueue.main.async {
                        imageView.image = image
                    }
                } else {
                    // You should be giving an option to retry the image here
                    imageView.image = placeholder
                }
            }
            }.resume()
    }
}


    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let w = self.view.bounds.width - 30

        return CGSize(width: w, height: w + 60)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return arrURLs.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCollectionViewCell", for: indexPath) as! DummyCollectionViewCell

        let str = arrURLs[indexPath.item]
        let url = URL(string: str)

        downloadImage(url: url!) { (img) in
            DispatchQueue.main.async {
                cell.imgView.image = img ?? UIImage(named: "placeholder")
            }
        }

        return cell
    }
}

输出GIF


由于堆栈的大小限制,上面的 gif 质量较低。如果您需要查看完整尺寸的 gif,请参考:https://imgur.com/tcjMWgc

那是因为电池是可重复使用的。

上面的单元格被重复使用,但单元格没有更新图像,因为单元格的图像已经设置。

您应该扩展 UIImage 以更新单元格的图像

像这样:

扩展 UIImageView {

func loadImageNone(_ urlString: String) {

    if let cacheImage = imageCache.object(forKey: urlString as NSString) {
        self.run(with: cacheImage)
        return
    } else {
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                completion(nil)
                return
            }

            if let respo  = response as? HTTPURLResponse {
                if let imageData = data, let image = UIImage(data: imageData) {

                  imageCache.setObject(image, forKey: urlString as NSString)

                  DispatchQueue.main.async {
                         self.image = image
                  }
                }
            }
        }.resume()
   }

func run(with image: UIImage) {
    UIView.transition(with: self,
                      duration: 0.5,
                      options: [],
                      animations: { self.image = image },
                      completion: nil)
     }
}

像下面这样改变你的方法

// This method is getting called for all the cells
func downloadImage(url: URL, imageView: UIImageView) {
    // Image is set if cache is available
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        // Reset the image to placeholder as the URLSession fetches the new image
        imageView.image = UIImage(named: "placeholder")
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error == nil else  {
                // You should be giving an option to retry the image here
                imageView.image = UIImage(named: "placeholder")
                return
            }

            if let respo  = response as? HTTPURLResponse {
                print("Status Code : ", respo.statusCode)
                if let imageData = data, let image = UIImage(data: imageData) {
                    self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
                    // Update the imageview with new data 
                    imageView.image = image
                } else {
                    // You should be giving an option to retry the image here
                    imageView.image = UIImage(named: "placeholder")
                }
            }
        }.resume()
    }
}

并在cellForItemAt里面调用它,比如

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DummyCollectionViewCell", for: indexPath) as! DummyCollectionViewCell

    let str = arrURLs[indexPath.item]

    if let url = URL(string: str) {
        downloadImage(url: url, imageView: cell.imgView)
    } else {
        cell.imgView.image = UIImage(named: "placeholder")
    }

    return cell
}

对于自定义 UICollectionViewCell class,您必须使用 super 调用 prepareForReuse()。 这确保为每一行调用出列并获取缓存。

override func prepareForReuse() {
    super.prepareForReuse()

    reuseAction()
}

From Apple Doc 此外,当图像下载时,您必须:

self.collectionView.reloadData()

或者如果您在图像完成加载时持有对行的引用,则重新加载行

let indexSet = IndexSet(integer: indexPath.section)
collectionView.reloadSections(indexSet)

我认为问题出在你的响应处理程序中,你正在为你请求的 url 设置缓存,而不是为来自响应的 url 设置缓存,我稍微修改了你的代码,试试看,希望它会帮助你

func downloadImage(url: URL, imageView: UIImageView, placeholder: UIImage? = nil, row: Int) {
    imageView.image = placeholder
    imageView.cacheUrl = url.absoluteString + "\(row)"
    if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
        imageView.image = cachedImage
    } else {
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard
                let response = response as? HTTPURLResponse,
                let imageData = data,
                let image = UIImage(data: imageData),
                let cacheKey = response.url?.absoluteString,
                let index = self.arrURLs.firstIndex(of: cacheKey)
                else { return }
            DispatchQueue.main.async {
                if cacheKey + "\(index)" != imageView.cacheUrl { return }
                imageView.image = image
                self.imageCache.setObject(image, forKey: cacheKey as NSString)
            }
            }.resume()
    }
}

var associateObjectValue: Int = 0
extension UIImageView {

    fileprivate var cacheUrl: String? {
        get {
            return objc_getAssociatedObject(self, &associateObjectValue) as? String
        }
        set {
            return objc_setAssociatedObject(self, &associateObjectValue, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}

更新:

USE THIS IMAGE LOADER EXTENSION 

let imageCache = NSCache<AnyObject, AnyObject>()

class ImageLoader: UIImageView {

    var imageURL: URL?

    let activityIndicator = UIActivityIndicatorView()

    func loadImageWithUrl(_ url: URL) {

        // setup activityIndicator...
        activityIndicator.color = .darkGray

        addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true

        imageURL = url

        image = nil
        activityIndicator.startAnimating()

        // retrieves image if already available in cache
        if let imageFromCache = imageCache.object(forKey: url as AnyObject) as? UIImage {

            self.image = imageFromCache
            activityIndicator.stopAnimating()
            return
        }

        // image does not available in cache.. so retrieving it from url...
        URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in

            if error != nil {
                print(error as Any)
                self.activityIndicator.stopAnimating()
                return
            }

            DispatchQueue.main.async(execute: {

                if let unwrappedData = data, let imageToCache = UIImage(data: unwrappedData) {

                    if self.imageURL == url {
                        self.image = imageToCache
                    }

                    imageCache.setObject(imageToCache, forKey: url as AnyObject)
                }
                self.activityIndicator.stopAnimating()
            })
        }).resume()
    }
}

            ** design controller  **

                 import UIKit

                class ImageController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

            private let cellId = "cellId"

                lazy var imagesSliderCV: UICollectionView = {

                    let layout = UICollectionViewFlowLayout()
                    layout.scrollDirection = .vertical
                    let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
                    cv.translatesAutoresizingMaskIntoConstraints = false
                    cv.backgroundColor = .white
                    cv.showsHorizontalScrollIndicator = false
                    cv.delegate = self
                    cv.dataSource = self
                    cv.isPagingEnabled = true
                    cv.register(ImageSliderCell.self, forCellWithReuseIdentifier: self.cellId)
                    return cv
                }()

             //
                // Mark:- CollectionView Methods........
                //
                var arrURLs = [
                    "https://homepages.cae.wisc.edu/~ece533/images/airplane.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/arctichare.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/baboon.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/barbara.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/boat.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/cat.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/fruits.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/frymire.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/girl.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/goldhill.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/lena.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/monarch.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/mountain.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/peppers.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/pool.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/sails.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/serrano.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/tulips.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/watch.png",
                    "https://homepages.cae.wisc.edu/~ece533/images/zelda.png"
                ]

                func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

                    return arrURLs.count
                }

                func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ImageSliderCell


                    let ImagePath = arrURLs[indexPath.item]
                       if  let strUrl = ImagePath.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
                        let imgUrl = URL(string: strUrl) {

                        cell.frontImg.loadImageWithUrl(imgUrl)
                    }
                    return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

                    return CGSize(width: screenWidth, height: 288)
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

                    return 0
                }


        func setupAutoLayout(){

                NSLayoutConstraint.activate([

                    imagesSliderCV.leftAnchor.constraint(equalTo: view.leftAnchor),
                    imagesSliderCV.rightAnchor.constraint(equalTo: view.rightAnchor),
                    imagesSliderCV.topAnchor.constraint(equalTo: view.topAnchor),
                    imagesSliderCV.bottomAnchor.constraint(equalTo: view.bottomAnchor),

                    ])

            }
        }

    **collectionView cell **

    import UIKit

    class ImageSliderCell: UICollectionViewCell {    

        //
        let frontImg: ImageLoader = {

            let img = ImageLoader()
            img.translatesAutoresizingMaskIntoConstraints = false
            img.contentMode = .scaleAspectFill
            img.clipsToBounds = true
            return img
        }()

        //
        override init(frame: CGRect) {
            super.init(frame: frame)

            addSubview(frontImg)
            setupAutolayout()
        }

        func setupAutolayout(){

            frontImg.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
            frontImg.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true
            frontImg.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
            frontImg.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8).isActive = true
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }

输出:-