Download and cache images with Combine
Here we will create a simple UIImage
extension that implements a function that can be called to download an image from an URL. First we need to preface this article by creating two functions as a part of an extension of URLCache
:
extension URLCache { // This function will store a cached URLResponse from raw image data. func cacheImage(from response: HTTPURLResponse, for request: URLRequest, data: Data) { let cachedImage = CachedURLResponse(response: response, data: data) storeCachedResponse(cachedImage, for: request) } // This function will return an image from a cached URL response. func image(from request: URLRequest) -> UIImage? { guard let data = cachedResponse(for: request)?.data, let image = UIImage(data: data) else { return nil } return image } }
These two functions will set an image to URLCache
and get an image from the URLCache
respectively. Lets get into code of this article. We are gonna create an extension of UIImage
and add a static function called load
with a url string and an escaping completion block returning an optional image as parameters. But first we have to make sure we import Combine
together with UIKit
at the top of the file:
import UIKit import Combine extension UIImage { // 1 private static var cancellables = Set<AnyCancellable>() static func load(from urlString: String, _ completion: @escaping (UIImage?) -> Swift.Void) { // 2 DispatchQueue.global(qos: .userInitiated).async { // 3 guard let imageURL = URL(string: urlString) else { DispatchQueue.main.async { completion(nil) } return } // 4 let cache = URLCache.shared // 5 let request = URLRequest(url: imageURL) // 6 if let image = cache.image(from: request) { DispatchQueue.main.async { completion(image) } } else { // 7 URLSession.shared.dataTaskPublisher(for: request) // 8 .tryMap { output -> Data in cache.cacheImage(from: output, for: request) return output.data } // 9 .tryMap { UIImage(data: $0) } // 10 .sink(receiveCompletion: { completed in // 11 switch completed { case .failure: DispatchQueue.main.async { completion(nil) } case .finished: break } }, receiveValue: { image in // 12 DispatchQueue.main.async { completion(image) } }) // 13 .store(in: &cancellables) } } } }
This is what the code does:
- Lets start by creating a private static var called
cancellables
, it’s gonna be a set of typeAnyCancellable
. This is used to store all our downloaded images. - Next we’ll create a background thread, preferably with a quality of service
userInitiated
because this load function can be used in a table view and load cell images while scrolling fast. - Try to create an URL from the url string otherwise dispatch the completion block on the main thread with a nil value.
- Get a shared instance of
URLCache
. - Create a
URLRequest
from the url string. - Check the cache if we already have an image, if we do dispatch the completion block on the main thread with the image.
- If the image isn’t in the cache and we haven’t downloaded it before, create a new
URLSession.shared.dataTaskPublisher
. - In the
tryMap
closure we cache the image and return the image data. - The in the next
tryMap
closure we create a newUIImage
from the image data. - Next we sink the result into a value.
- In the `receiveCompletion` block we switch over the completion and if we failed for some reason we dispatch the completion block in the main thread with a nil value.
- If we did receive a value (i.e our new image) we dispatch the completion block on the main thread with the image.
- Finally we store the cancellable in the set.
Now you can use this function like ths:
let urlString = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png" UIImage.load(from: urlString) { [weak self] image in self?.imageView.image = image }
Which will result in this image: