dahiya_boy
dahiya_boy

Reputation: 9503

Image downloading and caching issue

I am downloading images from server and showing it in collectionView. I am caching the Images so that user got fast server response and no glitches in UI. Until the image is not downloaded I added placeholder image too.

But in my output, the image is replicating in other cells and images is not caching in NSCache properly..

Here is the below code

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

Output GIF

enter image description here


Due to size restriction on stack, the above gif is in low quality. If you need to check gif in full size then please refer : https://i.sstatic.net/aFSxC.jpg

Upvotes: 0

Views: 1667

Answers (5)

kartik patel
kartik patel

Reputation: 536

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")
        }
    }

OUTPUT: -

enter image description here

Upvotes: 2

Alexandr Kolesnik
Alexandr Kolesnik

Reputation: 2204

I think the problem is in your response handler, you are setting cache for url you are requesting, not for url from response, I modified your code a little bit, try, hope it will help you

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

And

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

UPDATED:

Upvotes: 2

Cristi Ghinea
Cristi Ghinea

Reputation: 484

You have to call prepareForReuse() with super for a custom UICollectionViewCell class. This ensures the dequeue is called for each row and gets the cache.

override func prepareForReuse() {
    super.prepareForReuse()

    reuseAction()
}

From Apple Doc Also when the image downloads you have to either:

self.collectionView.reloadData()

or reload row if you hold a reference to the row when the image finishes to load

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

Upvotes: 0

Cerlin
Cerlin

Reputation: 6722

Change your method like below

// 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()
    }
}

And call it inside cellForItemAt like

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
}

Upvotes: 0

emily_
emily_

Reputation: 21

That's because cell is reusable.

Upper cell is reused, but cell is not updating image since cell's image already set.

You should extend UIImage to update cell's image

like this:

extension 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)
     }
}

Upvotes: 0

Related Questions