How to cache images using URLSession in Swift
Asked Answered
B

4

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
    }
}
Beasley answered 29/11, 2016 at 18:57 Comment(3)
nshipster.com/nsurlcacheMera
Unrelated, your if let strongSelf = self { strongSelf.image = image } can be simplified to self?.image = image.Blooming
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).Blooming
T
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
}
Tulipwood answered 23/6, 2017 at 6:19 Comment(0)
C
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

Caravette answered 10/3, 2017 at 8:4 Comment(0)
H
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.

Hist answered 18/7, 2021 at 19:14 Comment(2)
This should be the accepted answer. Let CFNetwork's battle-tested caching code do the work for you.Campanulaceous
if this post says the truth then yes, definitely no other cache management needed for small imagesMidterm
B
-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
                }
            }
        }
    }

 }
}
Bomber answered 3/1, 2020 at 7:23 Comment(1)
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.Rattray

© 2022 - 2024 — McMap. All rights reserved.