NS

Remote Image URL with Caching in SwiftUI

In this project, we will be loading an Image from Remote URL and implement Caching to prevent our application to make network requests over and over again.

We know SwiftUI currently does not allow something like,

Image("https://url-of-image")

Let’s get started on how to attain the functionality above. I will divide the definitions into three different files to make the code more readable.

  1. ContentView — This is your view where you pass a remote URL to ImageView.
  2. ImageView — To Render the Image from URL
  3. ImageModel — Image Fetch and Caching Logic.

Let us look at all the three files closely,

Content View:

We are simply adding an image here to the screen

  @State var url = "https://link-to-image"
  var body: some View {
    VStack {
      ImageView(url: url)
  } 
}

Image View:

    @ObservedObject var remoteImageModel: RemoteImageModel
    
    init(url: String?) {
        remoteImageModel = RemoteImageModel(imageUrl: imageUrl)
    }
    
    var body: some View {
        Image(uiImage: remoteImageModel.image ?? UIImage(named: "default-image-here")!)
            .resizable()
    }
}

RemoteImageModel:

This class is where all the magic happens. It takes the URL and run it through the cache. If its a cache hit, the image is returned from the cache, if not, we create an async request to load new data

    @Published var displayImage: UIImage?
    var imageUrl: String?
    var cachedImage = CachedImage.getCachedImage()
    
    init(imageUrl: String?) {
        self.imageUrl = imageUrl
        if imageFromCache() {
            return
        }
        imageFromRemoteUrl()
    }
    
    
    func imageFromCache() -> Bool {
        guard let url = imageUrl, let cacheImage = cachedImage.get(key: url) else {
            return false
        }
        displayImage = cacheImage
        return true
    }
    
    func imageFromRemoteUrl() {
        guard let url = imageUrl else {
            return
        }
        
        let imageURL = URL(string: url)!

        URLSession.shared.dataTask(with: imageURL, completionHandler: { (data, response, error) in
            if let data = data {
                DispatchQueue.main.async {
                    guard let remoteImage = UIImage(data: data) else {
                        return
                    }
                    
                    self.cachedImage.set(key: self.imageUrl!, image: remoteImage)
                    self.displayImage = remoteImage
                }
            }
            }).resume()
    }
}

class CachedImage {
    var cache = NSCache<NSString, UIImage>()
    
    func get(key: String) -> UIImage? {
        return cache.object(forKey: NSString(string: key))
    }
    
    func set(key: String, image: UIImage) {
        cache.setObject(image, forKey: NSString(string: key))
    }
}

extension CachedImage {
    private static var cachedImage = CachedImage()
    static func getCachedImage() -> CachedImage {
        return cachedImage
    }
}

That’s it! Your code works now!!

This feels like a lot of code to load an image from a remote URL. For the go-getters, just create a new file and name it “ImageViewController.swift” and copy over the following code to it and call it from ImageContentView.swift mentioned above.

import SwiftUI
struct ImageViewController: View {
    @ObservedObject var url: LoadUrlImage

    init(imageUrl: String) {
        url = LoadUrlImage(imageURL: imageUrl)
    }

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

class LoadUrlImage: ObservableObject {
    @Published var data = 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 {
            self.data = 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 {
                        self.data = data
                    }
                }
            }).resume()
        }
    }
}

SwiftUI is still very young and there isn’t a huge community and support on Stack Overflow and other such platforms. Let’s build a community together and support building amazing products!