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
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.
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
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
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
➌ We are using catch operator for error handling. It replaces the upstream publisher with
➍ 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
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:
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.
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.
Thanks for reading!