SimpleImageCache iOS Swift4 Refresh

Posted in software by Christopher R. Wirz on Wed Jun 06 2018



SimpleImageCache was last updated 4 years ago for iOS 7. Since then, developers have cloned this repo and updated it as Swift has evolved. With Swift 4 the new standard, it is time to dust off this class and use some new paradigms.

Note: This example adds functionality properties to UIImageView using a singleton pattern from the SimpleImageCache class.

Many of the alternative CocoaPods have not made the jump to iOS Swift4 yet, but luckily SimpleImageCache provides all the power you need.

The file UIImageView+SimpleImageCache.swift could be created as follows:


import Foundation
import UIKit

class SimpleImageCache: NSObject, URLSessionTaskDelegate {

    static let sharedInstance = SimpleImageCache()

    var urlSession: URLSession!
    var urlCache = URLCache(memoryCapacity: 64 * 1024 * 1024, diskCapacity: 512 * 1024 * 1024, diskPath: "ImageDownloadCache")
    var downloadQueue = Dictionary<URL, [(UIImage?, Error?)->()]>()

    override init(){
        super.init()

        let config = URLSessionConfiguration.default
        config.requestCachePolicy = URLRequest.CachePolicy.returnCacheDataElseLoad
        config.urlCache = urlCache

        self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }

    func getImageFromCache(url:URL) -> UIImage? {
        guard let urlRequest = self.makeRequest(url: url) else {return nil}
        if let response = urlCache.cachedResponse(for: urlRequest) {
            if let image = UIImage(data: response.data) {
                return image
            }
        }
        return nil
    }

    func getImage(url:URL, completion:((UIImage?, Error?)->())? = nil) {

        guard let urlRequest = self.makeRequest(url: url) else {return}

        if let image = self.getImageFromCache(url: url) {
            if let c = completion {
                DispatchQueue.main.async {
                    c(image, nil)
                    return
                }
            }
        } else {
            let sv = self
            OperationQueue.main.addOperation {
                let task = self.urlSession.dataTask(with: urlRequest) { (data, response, error) -> Void in
                    if let completionHandler = sv.downloadQueue[url] {
                        if let errorReceived = error {
                            DispatchQueue.main.async {
                                completionHandler.forEach{ c in
                                    c(nil, errorReceived)
                                }
                            }
                            return
                        } else {
                            if let httpResponse = response as? HTTPURLResponse {
                                if httpResponse.statusCode >= 400 {
                                    DispatchQueue.main.async {
                                        completionHandler.forEach{ c in
                                            c(nil, NSError(domain: NSURLErrorDomain, code: httpResponse.statusCode, userInfo: nil))
                                        }
                                    }
                                    return
                                } else if let d = data {
                                    if let r = response {
                                        sv.urlCache.storeCachedResponse(CachedURLResponse(response:r, data:d, userInfo:nil, storagePolicy: URLCache.StoragePolicy.allowed), for: urlRequest)
                                    }

                                    if let image = UIImage(data: d) {
                                        DispatchQueue.main.async {
                                            completionHandler.forEach{ c in
                                                c(image, nil)
                                            }
                                        }
                                        return
                                    }
                                } else {
                                    DispatchQueue.main.async {
                                        completionHandler.forEach{ c in
                                            c(nil, nil)
                                        }
                                    }
                                    return
                                }
                            }
                        }
                    }
                    sv.cancelImage(requestUrl: url)
                }
                sv.addToQueue(url: url, task: task, completion: completion)
            }
        }
    }

    func cancelImage(requestUrl:URL?) {
        if let url = requestUrl {
            if let index = self.downloadQueue.index(forKey: url) {
                self.downloadQueue.remove(at: index)
            }
        }
    }


    // MARK: - Private

    private func addToQueue(url:URL, task:URLSessionDataTask?, completion:((UIImage?, Error?)->())? = nil) {
        if let c = completion {
            if self.downloadQueue.keys.contains(url) {
                self.downloadQueue[url]?.append(c)
            } else {
                self.downloadQueue[url] = [c]
            }
        }
        if let t = task, t.state != .running {
            t.resume()
        }
    }

    private func makeRequest(url: URL) -> URLRequest? {
        return URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 30.0)
    }

    private func makeRequest(url: String) -> URLRequest? {
        if let u = URL(string: url) {
            return self.makeRequest(url: u)
        }
        return nil
    }
}

extension UIImageView {
    func setImage(url:String?, placeholderImage:UIImage?, success: @escaping (UIImage?)->()){
        guard let url = url, url.length > 5 else {
            if let i = placeholderImage {
                self.image = i
                success(i)
                return
            }
            success(nil)
            return;
        }
        if let u = URL(string: url) {
            if let i = SimpleImageCache.sharedInstance.getImageFromCache(url: u) {
                self.image = i
                success(i)
                return
            } else if let i = placeholderImage {
                self.image = i
            }
            SimpleImageCache.sharedInstance.getImage(url: u, completion: { imageSrc, error in
                if let i = imageSrc {
                    self.image = i
                    success(i)
                } else {
                    success(nil)
                }
            })
        }
    }
    func setImage(url:String?, placeholderImage:UIImage? = nil){
        self.setImage(url: url, placeholderImage: placeholderImage){ result in
            // Do nothing
        }
    }
}

So what next? How about we try it on a UIViewController...

Note: The code below has only been tested in iOS Swift 4.1.

import UIKit

class SimpleCacheDemoViewController: UIViewController {

    let imageView: UIImageView = UIImageView()
    private var _loaded = false

    override func viewDidLoad() {
        super.viewDidLoad()

        if _loaded {return}
        _loaded = true
        imageView.frame = CGRect(x: (UIScreen.main.bounds.width - 250.0) / 2, y: 32.0, width: 250.0, height: 250.0)
        imageView.setImage(url: "https://www.mywebsite.com/myimage.png", placeholderImage: UIImage(named: "loading_image"))
        view.addSubview(imageView)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
}

There you have it. The downloaded image will load as soon as the view is loaded. If the image has been downloaded before, and has an extended cache policy from the server, it will load instantly.