October 3, 2020

How to make NSCache do its job

Recently, I needed to add caching to one of the iOS apps I’m working on. While researching a few possible ways how to go about this I came across a great article written by John Sundell (a pretty common occurrence when searching for Swift topics on the web, thanks John!). While the basic concept he describes in the article worked quite well for my needs, there was one major problem with the approach: NSCache will drop your objects like hot potatoes as soon as your app gets backgrounded.

This behavior essentially nullifies the purpose of implementing caching in the first place. I guess most developers have the same assumptions as I had about how NSCache behaves:

  1. When memory is needed, the system will automatically remove objects from your cache.
  2. Your cached objects survive your app being backgrounded

As already mentioned, the latter isn’t true. At least not for the particular implementation described in the original article.

Luckily, there’s an easy yet somehow undocumented fix for this situation. This StackOverflow answer pointed me in the right direction. The user writes:

In addition, based on source of NSCache.m I found here, objects that do not conform to NSDiscardableContent protocol are never removed at runtime even the app needs more memory and should evict some of its elements. Maybe that’s the reason why non-NSDiscardableContent objects are evicted when the app is entering background because that’s a good time for them to be evicted.

So if you don’t want the system to evict all objects from your cache when your app gets backgrounded, make sure your objects implement the NSDiscardableContent protocol.

In case of John’s original example, conforming to NSDiscardableContent is quite trivial:

private extension Cache: NSDiscardableContent {
    final class Entry {
        let value: Value

        init(value: Value) {
            self.value = value
        func beginContentAccess() -> Bool { true }
        func endContentAccess() { }
        func discardContentIfPossible() { }
        func isContentDiscarded() -> Bool { false }

Enjoy your data waiting for you in the cache after you come back from liking stuff on Instagram!