13

I would like to enhance the code below to cache images and only download them if they haven't been cached previously. I can't seem to find any good examples of how to use URLSession object to do this.

extension UIImageView {
    func loadImageWithURL(_ url: URL) -> URLSessionDownloadTask {
        let session = URLSession.shared

        let downloadTask = session.downloadTask(with: url, completionHandler: { [weak self] url, response, error in

            if error == nil, let url = url,
                let data = try? Data(contentsOf: url), let image = UIImage(data: data) {

                    DispatchQueue.main.async {
                        if let strongSelf = self {
                            strongSelf.image = image
                        }
                    }
            }
        })
        downloadTask.resume()
        return downloadTask
    }
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Martin Muldoon
  • 3,388
  • 4
  • 24
  • 55
  • 1
    http://nshipster.com/nsurlcache/ – Leo Dabus Nov 29 '16 at 19:04
  • 1
    Unrelated, your `if let strongSelf = self { strongSelf.image = image }` can be simplified to `self?.image = image`. – Rob Nov 29 '16 at 22:00
  • 1
    FWIW, `NSURLCache` will cache according to what is supplied in the headers of the server response. Also, it will constrain cache according to poorly documented rules, most notably if a download exceeds 5% of the total cache, it won't cache it, regardless of what the server's headers say (this is one reason to for bump the cache size as outlined in the link Leo provided). – Rob Nov 29 '16 at 22:07

4 Answers4

23

Updated for Swift 4

import UIKit

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)
                DispatchQueue.main.async(execute: {
                    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()
    }
}

Usage:

// assign ImageLoader class to your imageView class
let yourImageView: ImageLoader = {

    let iv = ImageLoader()
    iv.frame = CGRect(x: 10, y: 100, width: 300, height: 300)
    iv.backgroundColor = UIColor(red: 0.94, green: 0.94, blue: 0.96, alpha: 1.0)
    iv.contentMode = .scaleAspectFill
    iv.clipsToBounds = true
    return iv
}()


// unwrapped url safely...
   if let strUrl = "https://picsum.photos/300/300".addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
      let imgUrl = URL(string: strUrl) {

      yourImageView.loadImageWithUrl(imgUrl) // call this line for getting image to yourImageView
}
iAj
  • 3,787
  • 1
  • 31
  • 34
9

One potential solution to this would be to utilize NSCache to take care of caching. Essentially what you would do is check if you already have the image locally to load from rather than fetching every time before you actually make a request.

Here's one of my implementations, however - it's a subclass rather than an extension:

class CustomImageView: UIImageView {

    // MARK: - Constants

    let imageCache = NSCache<NSString, AnyObject>()

    // MARK: - Properties

    var imageURLString: String?

    func downloadImageFrom(urlString: String, imageMode: UIViewContentMode) {
        guard let url = URL(string: urlString) else { return }
        downloadImageFrom(url: url, imageMode: imageMode)
    }

    func downloadImageFrom(url: URL, imageMode: UIViewContentMode) {
        contentMode = imageMode
        if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) as? UIImage {
            self.image = cachedImage
        } else {
            URLSession.shared.dataTask(with: url) { data, response, error in
                guard let data = data, error == nil else { return }
                DispatchQueue.main.async {
                    let imageToCache = UIImage(data: data)
                    self.imageCache.setObject(imageToCache!, forKey: url.absoluteString as NSString)
                    self.image = imageToCache
                }
            }.resume()
        }
    }
}

Additionally, here's a useful resource: https://www.hackingwithswift.com/example-code/system/how-to-cache-data-using-nscache

Brandon Lee
  • 194
  • 2
  • 12
4

URLSession DataTask by default will cache the image automatically and you don't need to do anything on client-side as long as the cache setting on the server is normal. Images are static assets and won't change in short time, as the result, server will normally set "Cache-Control" to "public, max-age:xxxxx". URLSession default cache policy will cache the image both in memory and disk. However, it won't cache the image whose size is larger than 5% of disk size allocated for URLCache, and it doesn't do caching in background thread either.

James Wang
  • 341
  • 3
  • 6
-1
let imageCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {

    func loadImageFromUrl(urlString: String)  {
        if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage{
            self.image = imageFromCache
            return
        }

        Alamofire.request(urlString, method: .get).response { (responseData) in
            if let data = responseData.data {
               DispatchQueue.main.async {
                if let imageToCache = UIImage(data: data){
                    imageCache.setObject(imageToCache, forKey: urlString as AnyObject)
                    self.image = imageToCache
                }
            }
        }
    }

 }
}
Timbus Calin
  • 13,809
  • 5
  • 41
  • 59
Nishad S
  • 9
  • 2
  • 3
    Hello Welcome to StackOverflow, Please read questions properly before answering - It is clearly mentioned in question that it needs to be done using URLSession and not by any Third Party Libraries. – Abhirajsinh Thakore Jan 03 '20 at 07:30