Skip to content

Instantly share code, notes, and snippets.

@alecdoconnor
Last active March 8, 2019 16:19
Show Gist options
  • Save alecdoconnor/6e7328d3c060ca02f17a31a7739f4075 to your computer and use it in GitHub Desktop.
Save alecdoconnor/6e7328d3c060ca02f17a31a7739f4075 to your computer and use it in GitHub Desktop.
//
// ImageCache.swift
//
// Created by Alec O'Connor on 3/6/18.
// Copyright © 2018 Alec O'Connor. All rights reserved.
//
import UIKit
class ImageCache {
static let shared = ImageCache()
private init() { }
let cache = NSCache<NSString, UIImage>()
func getImage(from url: URL, completion: @escaping ((UIImage?, URL)->())) {
if let image = cache.object(forKey: url.absoluteString as NSString) {
// Return the cached image in the completion handler
completion(image, url)
} else {
// The image needs to be requested from the network
DispatchQueue.global(qos: .userInteractive).async {
do {
let imageData = try Data(contentsOf: url)
if let image = UIImage(data: imageData) {
// Successful download, save it and return the image in the completion handler
self.cache.setObject(image, forKey: url.absoluteString as NSString)
completion(image, url)
} else {
completion(nil, url)
}
} catch {
completion(nil, url)
}
}
}
}
}
@alecdoconnor
Copy link
Author

alecdoconnor commented Aug 29, 2018

I chose to make this its own class/singleton instead of adding as a static function in UIImageView, since it can be used in more places and should be centralized elsewhere. If you would like to use this directly on a UIImageView instance, add this extension in addition:

extension UIImageView {
    func setFromCache(withURL url: URL, placeholderImage: UIImage? = nil) {
        if let placeholderImage = placeholderImage {
            self.image = placeholderImage
        }
        ImageCache.shared.getImage(fromURL: url) { [weak self] (image, url) in
            if let image = image {
                self?.image = image
            }
        }
    }
}

@alecdoconnor
Copy link
Author

alecdoconnor commented Aug 29, 2018

The only caveat with setting images with a delayed completion is that you can run into a race conflict if you set different images on the same UIImageView at close times. This can happen, for example, when a user is scrolling and dequeue-able cells are sliding in and out. If a cell's UIImageView is loading, the same instance may appear in the wrong cell since it was dequeued shortly after requesting an image.

While it is not recommended to use accessibilityIdentifiers this way, you could use the following to prevent it:

extension UIImageView {
    func setFromCache(withURL url: URL, placeholderImage: UIImage? = nil) {
        if let placeholderImage = placeholderImage {
            self.image = placeholderImage
        }
        self.accessibilityIdentifier = url.absoluteString
        ImageCache.shared.getImage(fromURL: url) { [weak self] (image, url) in
            if let image = image,
            self?.accessibilityIdentifier == url.absoluteString {
                // only update this UIImageView's image if it is still needed
                self?.image = image
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment