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 : AnyObjectBoth key and value types must conform to AnyObject, which means reference types only.
This design aligns with how Foundation caching APIs behave:
NSCacheis an Objective-C API under the hood, where caching containers operate on object references.- 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 = 200This works well when cached objects are roughly similar in size.
totalCostLimit
totalCostLimit sets the approximate maximum total cost:
cache.totalCostLimit = 20 * 1024 * 1024 // ~20MB guidelineThis 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:
NSCacheis 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.
