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:

  1. Lets start by creating a private static var called cancellables, it’s gonna be a set of type AnyCancellable. This is used to store all our downloaded images.
  2. 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.
  3. Try to create an URL from the url string otherwise dispatch the completion block on the main thread with a nil value.
  4. Get a shared instance of URLCache.
  5. Create a URLRequest from the url string.
  6. Check the cache if we already have an image, if we do dispatch the completion block on the main thread with the image.
  7. If the image isn’t in the cache and we haven’t downloaded it before, create a new URLSession.shared.dataTaskPublisher.
  8. In the tryMap closure we cache the image and return the image data.
  9. The in the next tryMap closure we create a new UIImage from the image data.
  10. Next we sink the result into a value.
  11. 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.
  12. If we did receive a value (i.e our new image) we dispatch the completion block on the main thread with the image.
  13. 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: