Reusable Image Cache in Swift

Maksym Shcheglov
4 min readNov 3, 2019

--

Almost every application contains some kind of graphics. That is the reason why downloading and displaying images in a mobile application is one of the most common tasks for app developers. Eventually, it could be a source of unnecessary work when the application reloads the same images multiple times.

In this article, I’ll show how to improve it by creating an Image Cache and integrate it with Image Loader using Combine framework.

Using NSCache as a storage

While building a caching mechanism in iOS project most of the times you’d consider using NSCache class. There are quite some pros of this class such as it being thread-safe and removing items from cache when memory is needed by other applications, but also some cons about it like having unclear eviction process.

In any case it is a better option for caching comparing to collection classes from the Swift standard library or Foundation framework. In this article, we’ll be using NSCache as internal image cache storage. As an alternative, you can replace it with any other solution that follows one of the cache replacement policies.

Image rendering pipeline

If your app downloads images from the web, a common challenge is application responsiveness and performance. For example you might have a stutter while scrolling a table view with images. The issue is that image rendering doesn’t happen at once when assigning one to be displayed by an image view. The image rendering pipeline consists of several steps:

  • loading — loads compressed image into memory;
  • decoding — converts encoded image data into per pixel image information;
  • rendering — copies and scales the image data from the image buffer into the frame buffer.

It can add up to a significant amount of work on the main thread, making your app unresponsive. You can probably think of some potential improvements like decoding & rendering an image before one is assigned to the UIImageView.

This function consumes a regular UIImage and returns a decompressed and rendered version. It makes sense to have a cache of decompressed images. This should improve drawing performance, but with the cost of extra storage.

If you’d like to dive deeply into this topic I’d suggest watching the WWDC talk iOS Memory Deep Dive and Image and Graphics Best Practices.

In-memory Image Cache

It might be a good idea to start with defining the Image Cache requirements. The cache should implement CRUD functions (create, read, update, and delete). It would be nice to have a subscript and make our code more readable. Most of the times we’ll be caching an image loaded from the network, that’s why it makes perfect sense using URL as a key. Eventually we can declare the ImageCacheType as:

Image Cache implementation

Taking all of this into account we can declare the ImageCache class. Internally it has two NSCache fields to store compressed images and decompressed ones. We limit the cache size with the maximum number of objects and the total cost, such as the size in bytes of all images. The NSLock instance is used to provide mutually exclusive access and make the cache thread-safe.

Now we should implement several functions to satisfy the ImagecacheType requirements defined above. Here is the way we can insert and remove images from cache:

You might notice that we are setting the cost for the decoded image. Right, the decodedImageCache is configured with totalCostLimit. It should remove some elements when the total cost exceeds the maximum allowed one.

To get an image from cache first we should check for the decoded one as the best-case scenario. Next search for an image in the imageCache or return nil as a fallback.

We can use the functions above to define a subscript for the ImageCache:

Just like that, we’ve built the image cache that could be reused within your projects and make them faster and more responsive.

Integration with Image Loader

Let’s have a look at how to integrate ImageCache into your project. Let’s assume that you have Image Loader defined already. If not, it could be done as following using Combine framework:

dataTaskPublisher creates a publisher that delivers the results of performing URL session data tasks. It returns down the pipeline a tuple (data: Data, response: URLResponse).

➋ The map operator is used to create an optional UIImage object.

➌ We are using catch operator for error handling. It replaces the upstream publisher with Just(nil) publisher.

➍ Performs the work on the background queue.

➎ Switches to receive the image on the main queue.

eraseToAnyPublisher does type erasure on the chain of operators so the loadImage(from:) function returns an object of type AnyPublisher<UIImage?, Never>.

Next we should make some adjustments in the ImageLoader to return an image immediately if we have one and to cache one when date loading is finished. Eventually, the ImageLoader can look like:

➊ Returns Just publisher with the cached image if any.

➋ The data is passed into receiveOutput as the publisher makes it available. Here we cache an image as soon as data loading is done and dataTaskPublisher emits a new value.

Conclusion

With ImageCache, you can optimize the image loading within an app and enhance user experience. After all, loading images from cache should be always faster than getting ones from the network.

It should be mentioned that you can introduce some improvements to the solution mentioned above:

  • using LRU cache instead of NSCache;
  • adding persistence;
  • using read-write lock for better performance.

You can find the source code of everything described in this blog post on Github. Feel free to play around and reach me out on Twitter if you have any questions, suggestions or feedback.

Thanks for reading!

--

--

Maksym Shcheglov

Software Engineer with more than a decade of experience · Author of http://OnSwiftWings.com · Follow me on twitter.com/sgl0v