NSCache in Swift: A Practical Guide


Greetings, traveler!

Caching rarely looks like a hard problem—until an app starts rendering heavy feeds, repeatedly mapping rich text, or decoding the same assets on every scroll. At that point, the difference between “working” and “smooth” is often a cache layer placed in the right spot.

On iOS, NSCache is a great fit for this kind of workload. It offers a familiar key-value API, but it’s built around a crucial idea: cache entries are optional and disposable, especially under memory pressure.

This article explains what NSCache is, how it behaves, and how to use it safely in production code.

What is NSCache?

NSCache is an in-memory cache container from Foundation. Conceptually, it resembles a Dictionary, but it is designed for caching, not storage.

The key property of NSCache is that it can automatically evict items when the system needs memory. That makes it suitable for objects that are expensive to build, but safe to rebuild.

Data lifetime: tied to the process

NSCache does not persist between launches. Its content exists only while the app process is alive.

Practical implications:

  • If the app gets terminated, the cache is gone.
  • If the system reclaims memory aggressively, cached values may disappear.
  • A cache hit is never guaranteed.

That’s exactly the contract you want for performance optimizations: improve responsiveness when possible, never break correctness when not.

Why NSCache only stores class instances

The generic signature is:

class NSCache<KeyType, ObjectType> where KeyType : AnyObject, ObjectType : AnyObject

Both key and value types must conform to AnyObject, which means reference types only.

This design aligns with how Foundation caching APIs behave:

  1. NSCache is an Objective-C API under the hood, where caching containers operate on object references.
  2. Eviction becomes straightforward: removing an entry simply releases a reference, without involving value copying semantics.

If you need to cache value types, you typically use a wrapper class (“boxing”) or choose another caching mechanism.

A practical example: NSCache<NSNumber, NSAttributedString>

Throughout the article we’ll use:

let cache = NSCache<NSNumber, NSAttributedString>()

NSAttributedString is a practical cache value because it often represents work that is genuinely expensive:

  • heavy text mapping (Markdown/HTML → attributed string)
  • embedded content (NSTextAttachment) such as inline images

And NSNumber is a convenient key type for numeric identifiers or stable hashes.

import Foundation

final class NSAttributedStringCache {
    nonisolated(unsafe) static let shared = HTMLAttributedStringCache()

    private let cache = NSCache<NSNumber, NSAttributedString>()

    private init() {
        cache.countLimit = 500 // limit number of cached items
        cache.totalCostLimit = 15 * 1024 * 1024 // ~15 MB budget
    }

    func object(for id: Int) -> NSAttributedString? {
        cache.object(forKey: NSNumber(value: id))
    }

    func set(_ object: NSAttributedString, for id: Int) {
        let cost = object.length // simple cost approximation
        cache.setObject(object, forKey: NSNumber(value: id), cost: cost)
    }

    func remove(for id: Int) {
        cache.removeObject(forKey: NSNumber(value: id))
    }

    func removeAll() {
        cache.removeAllObjects()
    }
}

Where NSCache fits well (and where it doesn’t)

Great use cases

NSCache works well for computed objects such as:

  • rich text mapping results (NSAttributedString)
  • decoded images (UIImage)
  • parsing outputs
  • layout precomputation artifacts
  • view layer render results

A good rule of thumb: cache objects that are expensive to produce, but safe to reproduce.

Bad use cases

NSCache is not a good fit for:

  • anything that must survive app restarts
  • TTL-based caching (“keep for 5 minutes”)
  • critical data that must always be available
  • cache layers that require deterministic retention

Most importantly: NSCache is not a network cache.

Why NSCache is not a network cache (use URLCache instead)

Network caching is a separate domain.

NSCache does not provide:

  • disk persistence
  • HTTP cache semantics
  • header-aware behavior (Cache-Control, ETag, Last-Modified)
  • validation/revalidation
  • proper request/response keying

For networking, use URLCache, which integrates with URLSession.

let cache = URLCache(
    memoryCapacity: 50 * 1024 * 1024,
    diskCapacity: 200 * 1024 * 1024,
    directory: .cachesDirectory
)

let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .useProtocolCachePolicy

let session = URLSession(configuration: config)

Automatic eviction: avoid manual memory warning handling

Many codebases still manually clear caches on memory warnings:

NotificationCenter.default.addObserver(
    forName: UIApplication.didReceiveMemoryWarningNotification,
    object: nil,
    queue: .main
) { _ in
    cache.removeAllObjects()
}

In practice, this pattern is unnecessary when using NSCache. The container itself is already designed to react to memory pressure and reduce its footprint automatically.

Clearing everything manually can be reasonable for explicit state resets (e.g. logout), but it shouldn’t be your baseline cache strategy.

Configuring eviction: cost and limits

NSCache does not enforce limits by default. In production apps you should almost always configure them.

countLimit

countLimit sets the approximate maximum number of cached items:

cache.countLimit = 200

This works well when cached objects are roughly similar in size.

totalCostLimit

totalCostLimit sets the approximate maximum total cost:

cache.totalCostLimit = 20 * 1024 * 1024 // ~20MB guideline

This is useful when objects vary significantly in size.

Per-object cost

When inserting, you can provide a cost:

let key = NSNumber(value: postID)
cache.setObject(attributedString, forKey: key, cost: attributedString.length)

The cost does not need to represent bytes. It is simply a value that helps the cache make eviction decisions.

For NSAttributedString, common strategies include:

  • character count (length)
  • character count plus a heuristic for attachments
  • estimated memory footprint (if cheap to compute)

Thread safety

NSCache is thread-safe for basic get/set/remove operations.

However, the moment you wrap cache calls into multi-step flows, races can appear:

  • “check → compute → store”
  • tracking hit/miss counters elsewhere
  • additional metadata tables
  • TTL logic

At that point, you should treat the cache store as a concurrency-sensitive component.

If your cache layer:

  • maintains additional state
  • performs multi-step policies
  • requires strict consistency

…then add synchronization.

A good mental model:

NSCache is thread-safe. Your caching policy might not be.

Conclusion

NSCache is one of the cleanest ways to introduce non-critical in-memory caching on iOS. It is fast, thread-safe, and memory-pressure aware.

Use it when:

  • objects are expensive to build
  • cache misses are acceptable
  • values can be recreated safely

Avoid it when:

  • persistence is required
  • strict retention policies are needed
  • you need HTTP caching semantics

A well-scoped NSCache often eliminates reprocessing spikes and scrolling stutters—while staying flexible under memory pressure.