Use NSCache in SwiftUI

SwiftUI does not currently provide a built-in caching mechanism, even though caching is often essential for scenarios such as:

  • Storing downloaded images, so they do not need to be fetched repeatedly.
  • Storing expensive computed data—like attributed text—that involves complex rendering or transformations.

While we might consider other solutions, NSCache is a robust option. However, NSCache stipulates that both its keys and values must be objects (AnyObject). This poses a barrier in SwiftUI projects where data is frequently represented by structs, which are not classes and thus not directly compatible with AnyObject. Fortunately, Swift types can conform to Hashable, allowing you to translate them into a form suitable for NSCache.

To make a hashable struct usable as an NSCache key, you can convert it to an NSNumber based on its hash value. Below is a simple extension to AnyHashable:

extension AnyHashable {
    var nsCacheKey: NSNumber {
        // Convert the hashValue of the underlying type into an NSNumber.
        NSNumber(integerLiteral: hashValue)
    }
}

Because AnyHashable can wrap any hashable value, this extension makes it easy to generate an object key for NSCache. The type NSNumber neatly accommodates the integer-based hashValue, though you could pick another object-based solution if you prefer.

Example: Caching Downloaded Images

Here is a more elaborate demonstration of using NSCache with a hashable struct (in this case, several related properties wrapped in your own type). This struct will conform to Hashable.

// 1. Define a compound hashable struct representing the image request with multiple attributes.
struct CompoundImageRequest: Hashable {
    let url: URL
    let identifier: String
    let version: Int
}

This CompoundImageRequest includes a URL, an identifier, and a version number. By conforming to Hashable, Swift will synthesise a hash function that combines these properties, making the struct uniquely identifiable based on its contents.

Updated Cache and View Using the Compound Key

We now adjust our caching logic and SwiftUI view to utilise CompoundImageRequest as the cache key.

// 2. A wrapper class around NSCache, storing images using AnyHashable keys.
final class ImageCache {
    static let shared = ImageCache()
    private let cache = NSCache<NSNumber, UIImage>()
    
    private init() {}
    
    // Retrieve the image from the cache using a compound key
    func image(for request: AnyHashable) -> UIImage? {
        cache.object(forKey: request.nsCacheKey)
    }
    
    // Save the image to the cache using a compound key
    func insertImage(_ image: UIImage, for request: AnyHashable) {
        cache.setObject(image, forKey: request.nsCacheKey)
    }
    // Add other functions such as deletetion as needed.
}

// 3. A SwiftUI view that fetches and caches images with a compound key.
struct CompoundCachingImageView: View {
    let request: CompoundImageRequest
    @State private var image: UIImage?
    
    var body: some View {
        Group {
            if let uiImage = image {
                Image(uiImage: uiImage)
                    .resizable()
                    .scaledToFit()
            } else {
                ProgressView("Loading…")
            }
        }
        .onAppear {
            // Check if the image is already cached with the compound key
            if let cachedImage = ImageCache.shared.image(for: request) {
                image = cachedImage
                return
            }
            
            // Otherwise, download the image and store it in the cache
            Task {
                do {
                    let (data, _) = try await URLSession.shared.data(from: request.url)
                    if let uiImage = UIImage(data: data) {
                        ImageCache.shared.insertImage(uiImage, for: request)
                        image = uiImage
                    }
                } catch {
                    // Handle download error appropriately
                }
            }
        }
    }
}

In this example:

  1. CompoundImageRequest is a hashable struct containing multiple properties (URL, identifier, version). Its Hashable conformance allows it to be uniquely represented by a hash value derived from all its components.
  2. Caching Mechanism: The CompoundImageCache class utilises NSCache<NSNumber, UIImage> and converts the compound struct into an NSNumber key using the extension on AnyHashable. This approach preserves NSCache’s requirements while allowing the use of a multi-attribute key.
  3. CompoundCachingImageView checks the cache before making a network request, demonstrating a common SwiftUI pattern: load if not cached, and display the image when available.

This pattern can be adapted for many other caching needs in SwiftUI—whether you are caching data, text attributes, or other computed resources. By wrapping your hashable structs with AnyHashable and utilising the .nsCacheKey computed property, you can transform conventional Swift structs into NSCache-compatible keys with minimal extra code while keeping the mapping logic simple.

Leave a Comment

Scroll to Top