0

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.stack.imgur.com/aFSxC.jpg

dahiya_boy
  • 9,298
  • 1
  • 30
  • 51
  • Set the place holder of the `UIImageView` in the else statement of `downloadImage` method – Cerlin Jul 29 '19 at 10:00
  • @CerlinBoss In which line you taking about? – dahiya_boy Jul 29 '19 at 10:03
  • I have added it as an answer – Cerlin Jul 29 '19 at 10:08
  • you can use SDWebImage framework, it will do everything for you – Alexandr Kolesnik Jul 29 '19 at 11:04
  • @AlexandrKolesnik Thats what I am trying to learn how to work with NSCache. DO you have any solution regarding the issue as I already tried millions of solutions and combinations that i found on google. – dahiya_boy Jul 29 '19 at 11:10
  • all cells are reusable, that's why you should set default image in cellForRow before setting image from cache, than you should chain your cell.ImageView(you can use restorationIdentifier for it) with certain link and check it before set image to imageView – Alexandr Kolesnik Jul 29 '19 at 11:13
  • @AlexandrKolesnik Can you recheck updated `downloadImage` func. I am doing same what you said. If something is still missing then let me know I will be happy to try your answer. – dahiya_boy Jul 29 '19 at 11:16

5 Answers5

2

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:

dahiya_boy
  • 9,298
  • 1
  • 30
  • 51
Alexandr Kolesnik
  • 1,929
  • 1
  • 17
  • 30
  • I tried but images are replicating in another cell. – dahiya_boy Jul 29 '19 at 11:48
  • have you replaced your code with mine? try to reinstall app to clear your cache before usage – Alexandr Kolesnik Jul 29 '19 at 11:54
  • Can you please explain what was wrong ? And I also wanted to know we use cache to store temp values. So every time I launch image it's cache is cleaned and images are re-downloaded. But in SDWebImage, once image is added is cache then its remain in cache until we explictly clean it. Why? – dahiya_boy Jul 29 '19 at 12:30
  • As I said above cell is reusable and all its content too. In debug I noticed that the imageView with same address has load different urls, that's why I added runtime variable to update cache url according to cell row. You can look through the code of SDWebImage, I think they archive cache – Alexandr Kolesnik Jul 29 '19 at 12:33
2
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

kartik patel
  • 496
  • 1
  • 4
  • 15
0

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)
     }
}
emily_
  • 21
  • 3
  • The reason you have mentioned is correct, but simply animating the image change doesn't solve the issue. Also if statement doesn't require return as there is no code after the else statement – Cerlin Jul 29 '19 at 10:02
  • The overall difference b/w this answer and mine is, I am not doing as in extension. Rest of code work is same. P.S. I also tried Shobhakar Tiwari Answer from [here](https://stackoverflow.com/questions/37018916/swift-async-load-image) which is exactly same as your answer. – dahiya_boy Jul 29 '19 at 10:05
0

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
}
Cerlin
  • 6,622
  • 1
  • 20
  • 28
  • I changed my code with yours and still getting same response. Lots of answers and blogs have same answer that we are doing but dont know from where this glitch is coming from. – dahiya_boy Jul 29 '19 at 10:19
  • @dahiya_boy The code which u have posted in question has issue. The else statement will be waiting for the `URLSession` to be completed to set the image (Till this happens, the old image from reused cell will be displayed.) – Cerlin Jul 29 '19 at 10:22
  • ohh yeah thats the one of the point. Agreed but after adding your code still there is glitches. Once the all images is loaded into cache then it works properly. So there might be issue in time of getting the image from server. BTW if I relaunch the app then I got value from cache or not or I have to store cache explicitly to save the images in cache? – dahiya_boy Jul 29 '19 at 10:26
0

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)
Cristi Ghinea
  • 474
  • 1
  • 7
  • 20