Skip to Content
Course content

79: NSCache for In-Memory Caching

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a small photo gallery app recently, and I noticed something frustrating. Every time the user scrolled back up to a photo they'd already seen, the app would flicker for a millisecond while it re-loaded the image from the disk or the network. It felt janky. My first instinct was to just throw everything into a dictionary and call it a day.

The dictionary trap

I started with a simple dictionary. It seemed obvious: use the image URL as the key and the UIImage as the value. Here is how that looked in my head:

var imageCache: [URL: UIImage] = [:]

func getImage(url: URL) -> UIImage? {
    if let cachedImage = imageCache[url] {
        return cachedImage
    }
    // ... fetch image from network ...
    // imageCache[url] = downloadedImage
    return nil
}

For the first five minutes, it worked perfectly. The flickering stopped. But then I started testing with a larger dataset—hundreds of high-resolution photos. I opened the Memory Graph in Xcode and saw a vertical line climbing straight up. Because a Dictionary is a strong reference, it holds onto every single image I've ever downloaded until I manually remove them. On a device with limited RAM, this is a one-way ticket to an EXC_RESOURCE crash.

Switching to NSCache

I need something that acts like a dictionary but is "smarter" about the device's health. That's where NSCache comes in. It's basically a dictionary that knows how to purge itself when the system is running low on memory. I tried to swap it in, but I immediately hit a wall with Swift's type system.

// This doesn't compile!
let cache = NSCache<URL, UIImage>()

The compiler complained that URL doesn't conform to AnyObject. NSCache is an Objective-C legacy class, and it requires both the key and the value to be classes (objects), not structs. Since URL in Swift is a struct, it won't work. I had to use NSURL instead. It's a bit clunky, but it's the price of admission here.

let imageCache = NSCache<NSURL, UIImage>()

func getImage(url: URL) -> UIImage? {
    let nsUrl = url as NSURL
    if let cachedImage = imageCache.object(forKey: nsUrl) {
        return cachedImage
    }
    // ... fetch image ...
    // imageCache.setObject(downloadedImage, forKey: nsUrl)
    return nil
}

Tuning the eviction policy

Now the app won't crash under memory pressure, but I noticed that NSCache is a bit of a black box. It decides when to evict items based on its own internal logic. Sometimes it clears too much, and I'm back to that flickering problem. I wanted more control.

I found two properties that are actually quite useful: countLimit and totalCostLimit. The countLimit is the easy one—it just caps the number of objects. If I set it to 100, the 101st image will likely push out the oldest one.

let imageCache = NSCache<NSURL, UIImage>()
imageCache.countLimit = 100

But images aren't all the same size. A tiny thumbnail takes up way less room than a 4K wallpaper. That's where totalCostLimit comes in. It allows me to tell the cache, "Don't use more than 50MB of RAM." To make this work, I have to provide a "cost" whenever I save an object.

let imageCache = NSCache<NSURL, UIImage>()
imageCache.totalCostLimit = 50 * 1024 * 1024 // 50 MB

// When saving, I calculate the approximate byte size of the image
let bytes = downloadedImage.pngData()?.count ?? 0
imageCache.setObject(downloadedImage, forKey: nsUrl, cost: bytes)

By using totalCostLimit, I'm no longer guessing how many images will fit. I'm managing the actual memory footprint. It's a much more professional way to handle assets that are "nice to have" in memory but aren't critical to the app's basic survival.




📋 Practical Task

Implementing a Profile Picture Memory Cache

Build a ProfileImageManager class that handles the caching of user profile pictures. Your implementation must meet the following requirements:

  • Use NSCache to store the images, using NSURL as the key.
  • Set a countLimit of 50 images to prevent the cache from growing indefinitely.
  • Implement a method fetchImage(for url: URL) -> UIImage? that first checks the cache before returning nil (simulating a cache miss).
  • Implement a method saveImage(_ image: UIImage, for url: URL) that calculates the cost of the image (using image.pngData()?.count) and stores it in the cache.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.