In one of my older posts, I did something very similar with getting an image from an API. One of my readers mentioned that this was pretty wasteful as I wasn’t caching results, so I wanted to re-visit this and make a generic Image view that will do all the caching for you. What I want to do here is make some kind of image view that will download and cache images given a URL.

This is going to build off my apple news app I’ve been building, but this isn’t exclusive to that app. You should be able to read this post without context of the other posts and be able to understand this.

Displaying an Image from a URL

The first thing I need to create is a view that takes in a URL and displays the image at that URL. This view will have a model that takes in the URL, retrieves it, then saved it to a published variable. The view can then display a default image for when the data is loading, and it will automatically update when the image loads.

As for the downloading of the image, this is very easily done with a URLSession and UIImage. UIImage has a constructor that takes in the data returned from the URLSession and creates an image from it. The code for this look like the following:

struct UrlImageView: View { @ObservedObject var urlImageModel: UrlImageModel init(urlString: String?) { urlImageModel = UrlImageModel(urlString: urlString) } var body: some View { Image(uiImage: urlImageModel.image ?? UrlImageView.defaultImage!) .resizable() .scaledToFit() .frame(width: 100, height: 100) } static var defaultImage = UIImage(named: "NewsIcon") } class UrlImageModel: ObservableObject { @Published var image: UIImage? var urlString: String? init(urlString: String?) { self.urlString = urlString loadImage() } func loadImage() { loadImageFromUrl() } func loadImageFromUrl() { guard let urlString = urlString else { return } let url = URL(string: urlString)! let task = URLSession.shared.dataTask(with: url, completionHandler: getImageFromResponse(data:response:error:)) task.resume() } func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) { guard error == nil else { print("Error: \(error!)") return } guard let data = data else { print("No data found") return } DispatchQueue.main.async { guard let loadedImage = UIImage(data: data) else { return } self.image = loadedImage } } }

If you look closely, you’ll notice that we actually end up re-loading these images after scroll off screen. This is obviously not what we want to do, and we can very easily fix this with a cache.

Building a Cache

For our cache, we’ll do an in-memory cache using NSCache. This means that if our application is closed or the device needs to free up memory, the cache will be deleted. However, this is perfectly fine for our use case.

For something more advanced, I recommend checking out this post. John Sundell goes through building a general purpose cache that can be stored to the disk. I thought it was a very interesting read, and is definitely a more advanced cache than what I’m building.

Building out our cache looks easy. We’ll want to be able to cache a UIImage given a URL for that image. The assumption here is that the image from a URL is unique and wont change (which I think is a fair assumption). NSCache needs to take in an object for the Key, so we’ll have to wrap the URL String inside of an NSString.

To make all of this easier, we’ll wrap the cache inside an ImageCache class so that we can just pass in a String. This will also let us create a static instance of this cache that we can use in our ImageModel. You may want to inject the class instead of using a static instance, but this works for my purposes right now. The cache code looks like:

class ImageCache { var cache = NSCache<NSString, UIImage>() func get(forKey: String) -> UIImage? { return cache.object(forKey: NSString(string: forKey)) } func set(forKey: String, image: UIImage) { cache.setObject(image, forKey: NSString(string: forKey)) } } extension ImageCache { private static var imageCache = ImageCache() static func getImageCache() -> ImageCache { return imageCache } }

Using the Cache

At this point, we have an image cache and a class to download images. Now, let’s add this cache to our class.

First, we need to give our class access to our cache. Since I have a static method to retrieve this class, this is can be done by initializing a variable in our image class to this cache.

Next, we need to save our downloaded images to our class. In the getImageFromResponse function, we can add the image we retrieve to our cache. We’ll do this just after we validate that we have an image.

Finally, we need to try to hit our cache before we go to the URL. For this, we’ll create a new function, called loadImageFromCache, that loads the image and returns a boolean value to let us know if the value was loaded. We’ll call this function just before we try to load the image from the URL.

class UrlImageModel: ObservableObject { var imageCache = ImageCache.getImageCache() ... func loadImage() { if loadImageFromCache() { return } loadImageFromUrl() } func loadImageFromCache() -> Bool { guard let urlString = urlString else { return false } guard let cacheImage = imageCache.get(forKey: urlString) else { return false } image = cacheImage return true } ... func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) { ... DispatchQueue.main.async { guard let loadedImage = UIImage(data: data) else { return } self.imageCache.set(forKey: self.urlString!, image: loadedImage) self.image = loadedImage } } }

And as you can see, we now get a ton of cache hits.

Final Thoughts

This was exactly what I wanted. NSCache is a very simple to implement cache, and probably something most developers should look into using. I really like the general purpose cache that John Sundell wrote that used NSCache, and it seems like NSCache should have some of those features built into it (saving to memory, cache expiration).

Feel free to check out my github repo with this code, and check out my YouTube video on this:

Thanks for reading

– SchwiftyUI