15

I'm writing an app that needs to keep an in-memory cache of a bunch of objects, but that doesn't get out of hand so I'm planning on using NSCache to store it all. Looks like it will take care of purging and such for me, which is fantastic.

I'd also like to persist the cache between launches, so I need to write the cache data to disk. Is there an easy way to save the NSCache contents to a plist or something? Are there perhaps better ways to accomplish this using something other than NSCache?

This app will be on the iPhone, so I'll need only classes that are available in iOS 4+ and not just OS X.

Thanks!

Ajumal
  • 980
  • 10
  • 31
Cory Imdieke
  • 12,771
  • 8
  • 34
  • 46

4 Answers4

13

I'm writing an app that needs to keep an in-memory cache of a bunch of objects, but that doesn't get out of hand so I'm planning on using NSCache to store it all. Looks like it will take care of purging and such for me, which is fantastic. I'd also like to persist the cache between launches, so I need to write the cache data to disk. Is there an easy way to save the NSCache contents to a plist or something? Are there perhaps better ways to accomplish this using something other than NSCache?

You pretty much just described exactly what CoreData does; persistency of object graphs with purging and pruning capabilities.

NSCache is not designed to assist with persistency.

Given that you suggested persisting to a plist format, using Core Data instead isn't that big of a conceptual difference.

bbum
  • 160,467
  • 23
  • 266
  • 355
  • I've used Core Data on almost every database app I've created, but it just seems like this isn't the best match for it. I'm using the caches to store API results, keeping the app zippy on launch and when loading things that have already been loaded. I'm worried about the core data database getting out of hand and growing. It just doesn't seem that core data is best used for caching "temporary" data. The persistence I need is basically a secondary function to the in-memory caching functionality I need, which is why I was leaning towards NSCache. – Cory Imdieke Dec 28 '10 at 18:22
  • Yah -- I can see your conundrum. NSCoding isn't *that* hard to implement -- you could always go down that path. Then the question becomes when to write/update the persistent version of the cache. In my experience, it is pretty easy to head down a path that ends up re-inventing the persistency wheel with all its complexities. And, of course, the best performing application is the one that ships first. ;) – bbum Dec 28 '10 at 19:48
  • @CoryImdieke, we are facing the same situation now and I am planning to use NSCache. Just wondering which solution did you choose? – Koolala May 28 '13 at 23:33
  • This was a while back, but I believe we basically used an NSDictionary and wrote everything to a Plist, and just wrote our own cache purging routines to clean up the dictionary when necessary. We basically built our own NSCache-style object but added persistency to it. – Cory Imdieke May 31 '13 at 19:22
  • best answer now is use either CoreData or Realm. Coredata is most standard, Realm easier to learn. – Philip Fung Jan 09 '17 at 18:13
9

use TMCache (https://github.com/tumblr/TMCache). It's like NSCache but with persistence and cache purging. Written by the Tumblr team.

Philip Fung
  • 337
  • 5
  • 5
4

Sometimes it may be more convenient not to deal with Core Data and just to save cache content to disk. You can achieve this with NSKeyedArchiver and UserDefaults (I'm using Swift 3.0.2 in code examples below).

First let's abstract from NSCache and imagine that we want to be able to persist any cache that conforms to protocol:

protocol Cache {
    associatedtype Key: Hashable
    associatedtype Value

    var keys: Set<Key> { get }

    func set(value: Value, forKey key: Key)

    func value(forKey key: Key) -> Value?

    func removeValue(forKey key: Key)
}

extension Cache {
    subscript(index: Key) -> Value? {
        get {
            return value(forKey: index)
        }
        set {
            if let v = newValue {
                set(value: v, forKey: index)
            } else {
                removeValue(forKey: index)
            }
        }
    }
}

Key associated type has to be Hashable because that's requirement for Set type parameter.

Next we have to implement NSCoding for Cache using helper class CacheCoding:

private let keysKey = "keys"
private let keyPrefix = "_"

class CacheCoding<C: Cache, CB: Builder>: NSObject, NSCoding
where
    C.Key: CustomStringConvertible & ExpressibleByStringLiteral,
    C.Key.StringLiteralType == String,
    C.Value: NSCodingConvertible,
    C.Value.Coding: ValueProvider,
    C.Value.Coding.Value == C.Value,
    CB.Value == C {

    let cache: C

    init(cache: C) {
        self.cache = cache
    }

    required convenience init?(coder decoder: NSCoder) {
        if let keys = decoder.decodeObject(forKey: keysKey) as? [String] {
            var cache = CB().build()
            for key in keys {
                if let coding = decoder.decodeObject(forKey: keyPrefix + (key as String)) as? C.Value.Coding {
                    cache[C.Key(stringLiteral: key)] = coding.value
                }
            }
            self.init(cache: cache)
        } else {
            return nil
        }
    }

    func encode(with coder: NSCoder) {
        for key in cache.keys {
            if let value = cache[key] {
                coder.encode(value.coding, forKey: keyPrefix + String(describing: key))
            }
        }
        coder.encode(cache.keys.map({ String(describing: $0) }), forKey: keysKey)
    }
}

Here:

  • C is type that conforms to Cache.
  • C.Key associated type has to conform to:
    • Swift CustomStringConvertible protocol to be convertible to String because NSCoder.encode(forKey:) method accepts String for key parameter.
    • Swift ExpressibleByStringLiteral protocol to convert [String] back to Set<Key>
  • We need to convert Set<Key> to [String] and store it to NSCoder with keys key because there is no way to extract during decoding from NSCoder keys that were used when encoding objects. But there may be situation when we also have entry in cache with key keysso to distinguish cache keys from special keys key we prefix cache keys with _.
  • C.Value associated type has to conform to NSCodingConvertible protocol to get NSCoding instances from the values stored in cache:

    protocol NSCodingConvertible {
        associatedtype Coding: NSCoding
    
        var coding: Coding { get }
    }
    
  • Value.Coding has to conform to ValueProvider protocol because you need to get values back from NSCoding instances:

    protocol ValueProvider {
        associatedtype Value
    
        var value: Value { get }
    }
    
  • C.Value.Coding.Value and C.Value have to be equivalent because the value from which we get NSCoding instance when encoding must have the same type as value that we get back from NSCoding when decoding.

  • CB is a type that conforms to Builder protocol and helps to create cache instance of C type:

    protocol Builder {
        associatedtype Value
    
        init()
    
        func build() -> Value
    }
    

Next let's make NSCache conform to Cache protocol. Here we have a problem. NSCache has the same issue as NSCoder does - it does not provide the way to extract keys for stored objects. There are three ways to workaround this:

  1. Wrap NSCache with custom type which will hold keys Set and use it everywhere instead of NSCache:

    class BetterCache<K: AnyObject & Hashable, V: AnyObject>: Cache {
        private let nsCache = NSCache<K, V>()
    
        private(set) var keys = Set<K>()
    
        func set(value: V, forKey key: K) {
            keys.insert(key)
            nsCache.setObject(value, forKey: key)
        }
    
        func value(forKey key: K) -> V? {
            let value = nsCache.object(forKey: key)
            if value == nil {
                keys.remove(key)
            }
            return value
        }
    
        func removeValue(forKey key: K) {
            return nsCache.removeObject(forKey: key)
        }
    }
    
  2. If you still need to pass NSCache somewhere then you can try to extend it in Objective-C doing the same thing as I did above with BetterCache.

  3. Use some other cache implementation.

Now you have type that conforms to Cache protocol and you are ready to use it.

Let's define type Book which instances we will store in cache and NSCoding for that type:

class Book {
    let title: String

    init(title: String) {
        self.title = title
    }
}

class BookCoding: NSObject, NSCoding, ValueProvider {
    let value: Book

    required init(value: Book) {
        self.value = value
    }

    required convenience init?(coder decoder: NSCoder) {
        guard let title = decoder.decodeObject(forKey: "title") as? String else {
            return nil
        }
        print("My Favorite Book")
        self.init(value: Book(title: title))
    }

    func encode(with coder: NSCoder) {
        coder.encode(value.title, forKey: "title")
    }
}

extension Book: NSCodingConvertible {
    var coding: BookCoding {
        return BookCoding(value: self)
    }
}

Some typealiases for better readability:

typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>

And builder that will help us to instantiate Cache instance:

class BookCacheBuilder: Builder {
    required init() {
    }

    func build() -> BookCache {
        return BookCache()
    }
}

Test it:

let cacheKey = "Cache"
let bookKey: StringKey = "My Favorite Book"

func test() {
    var cache = BookCache()
    cache[bookKey] = Book(title: "Lord of the Rings")
    let userDefaults = UserDefaults()

    let data = NSKeyedArchiver.archivedData(withRootObject: BookCacheCoding(cache: cache))
    userDefaults.set(data, forKey: cacheKey)
    userDefaults.synchronize()

    if let data = userDefaults.data(forKey: cacheKey),
        let cache = (NSKeyedUnarchiver.unarchiveObject(with: data) as? BookCacheCoding)?.cache,
        let book = cache.value(forKey: bookKey) {
        print(book.title)
    }
}
mixel
  • 22,724
  • 10
  • 111
  • 154
0

You should try AwesomeCache. Its main features:

  • written in Swift
  • uses on on-disk caching
  • backed by NSCache for maximum performance and support for expiry of single objects

Example:

do {
    let cache = try Cache<NSString>(name: "awesomeCache")

    cache["name"] = "Alex"
    let name = cache["name"]
    cache["name"] = nil
} catch _ {
    print("Something went wrong :(")
}
mixel
  • 22,724
  • 10
  • 111
  • 154