lukecsmith.co.uk

Loading from URL and caching images in SwiftUI

Luke, 27 May 2020

Ive created a simple drop-in couple of classes that will handle all image loading and caching requirements, shared below. I needed all the usual stuff – first the ability to have a single line in a SwiftUI View which will load and show an image from a given URL. It also needs to check the local cache for that image first to save on repeated network calls. Should a network call be required, it needs to grab that image and that save it in the cache for next time.

Ive managed to use some Combine, as well as utilizing the excellent Apple option URLCache for persisting the images. Heres what usage of this class looks like :

struct ImageWithURL: View {
    
    @ObservedObject var imageLoader: ImageLoaderAndCache

    init(_ url: String) {
        imageLoader = ImageLoaderAndCache(imageURL: url)
    }

    var body: some View {
          Image(uiImage: UIImage(data: self.imageLoader.imageData) ?? UIImage())
              .resizable()
              .clipped()
    }
}

class ImageLoaderAndCache: ObservableObject {
    
    @Published var imageData = Data()
    
    init(imageURL: String) {
        let cache = URLCache.shared
        let request = URLRequest(url: URL(string: imageURL)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 60.0)
        if let data = cache.cachedResponse(for: request)?.data {
            print("got image from cache")
            self.imageData = data
        } else {
            URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                if let data = data, let response = response {
                let cachedData = CachedURLResponse(response: response, data: data)
                                    cache.storeCachedResponse(cachedData, for: request)
                    DispatchQueue.main.async {
                        print("downloaded from internet")
                        self.imageData = data
                    }
                }
            }).resume()
        }
    }
}

Its used very simply, like this :

struct ContentView: View {

    var body: some View {
        ImageWithURL("https://i.redd.it/mn5m2km7hmv01.jpg")
    }
}

Its making use of URLCache, which Apple provides as an ideal way to persist images to local disc without any of the hassle of things like CoreData. This cache will get purged, but only when the device is short of disc space.

To explain how it works : the ImageLoaderAndCache class at the bottom has an @Published property called imageData. Being @Published, it can stream changes to other properties that are observing it. Our ImageWithURL struct has this cache class as an @ObservedObject. This means that changes that happen within it, for example to @Published properties, can be acted on from within this struct. This means that our Image property within the struct can reference the imageData property from ImageLoaderAndCache, and when it changes, the image will update.

If you look through the process in the init of ImageLoaderAndCache, you can see the process of trying to first load from cache, and if not, load from URL using URLSession. If the request is successful, the image is then stored in the cache with the line ‘cache.storedCachedResponse(..’/. From then on, any requests for the same image (matching URL) can be taken from the cache succesfully rather than using another network call.

Please do comment with any ways this could be improved.

Tagged with: