TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Nov 2017 |
SwiftSwift Version | 4.0 |
SPMSupports SPM | ✗ |
Maintained by Zach Radke.
A flat cache implemented in Swift inspired by http://khanlou.com/2017/10/the-flat-cache/
Cache
as the single source of truthCached
values for changes over timeRelated
values that trigger observationsAdd the following line to your Podfile
:
pod 'Pancake'
Add the following line to your Cartfile
:
github "zradke/Pancake"
Identifiable
modelTo be inserted in a Cache
, a type need only be Identifiable
:
struct Book: Identifiable {
typealias ISBN = Int
var identifier: ISBN
var title: String
}
Any type can be used as the Identifier
as long as it conforms to CustomStringConvertible
, Hashable
, and Codable
. The Cache
works best with value types rather than reference types, so prefer struct
to class
for both the model and Identifier
.
Cache
Any Identifiable
value can be inserted into a Cache
:
let cache = Cache()
let book = Book(identifier: 9788700631625,
title: "Harry Potter and the Sorcerer's Stone")
cache.set(book)
Once in a Cache
, values can be retreived using the type's Identifier
:
let retreivedBook: Book? = cache.get(9788700631625)
Cached
valuesValues in a Cache
can also be wrapped as Cached
values:
let cachedBook: Cached<Book> = cache.cached(9788700631625)
Cached
values provide up-to-date values from a Cache
, but also can be observed:
// Retreives the latest value from the `Cache`
let currentValue = cachedBook.value
// Executes the closure whenever the value is changed in the `Cache`
let disposable = cachedBook.observe { (book) in
// update user interface etc.
}
Note that CachedValue.observe(_:)
returns a Cache.Disposable
which must be retained to keep the observation alive. Once it deallocates the observation automatically ends.
Mergeable
modelsAPIs often return incomplete representations of the same model. A Cache
can slowly build up a complete model if the type is Mergeable
:
struct Book: Identifiable, Mergeable {
typealias ISBN = Int
var identifier: ISBN
var title: String?
var publishedOn: Date?
func merged(with other: Book) -> Book {
var copy = self
if let title = other.title { copy.title = title }
if let publishedOn = other.publishedOn { copy.publishedOn = publishedOn }
return copy
}
}
When a Mergeable
value is inserted into the cache, it is merged with any existing value:
let bookStub = Book(identifier: 9788700631625,
title: "Harry Potter and the Sorcerer's Stone",
publishedOn: nil)
cache.set(bookStub)
// Later from a different API...
let detailedBook = Book(identifier = 9788700631625,
title: nil,
publishedOn: "1998-09-01".toDate())
cache.set(detailedBook)
let compositeBook: Book = cache.get(9788700631625)!
compositeBook.title // "Harry Potter and the Sorcerer's Stone"
compositeBook.publishedOn // 1998-09-01
Creating the Mergeable
implementations can be tedious with a large number of models, in which case Sourcery could be used.
HasCachedRelationships
Models often have relationships. The Cache
can help normalize the data by storing a single representation of all values and allowing generalized relationships. A type indicates it has relationships by conforming to HasCachedRelationships
, which is typically constructed by joining any Related
properties:
struct Author: Identifiable, HasCachedRelationships {
...
var books: Set<Related<Book>>
var relatedCacheKeys: Set<CacheKey> {
return books.map { $0.cacheKey }
}
}
struct Book: Identifiable, HasCachedRelationships {
...
var author: Related<Author>
var relatedCacheKeys: Set<CacheKey> {
return [author.cacheKey]
}
}
Related
values can be converted into Cached
values using Related.cached(in:)
to access their actualized value. However, a larger benefit of HasCachedRelationships
is that observers are notified when related objects change in the cache, which allows UI that depends on a nested value to always stay in sync:
let cache = Cache()
var author = Author(identifier: 1,
name: "JK Rowling")
let book = Book(identifier: 9788700631625,
title: "Harry Potter and the Sorcerer's Stone",
author: Related(author))
author.books.append(Related(book))
cache.set(author)
cache.set(book)
let disposable = cache.cached(book).observe { (value) in
// Update UI
}
author.bornOn = "1965-07-31".toDate()
cache.set(author) // Notifies the UI
The Cache
is smart enough to handle circular relationships and relationships of any depth (although go too deep and you may have performance problems).
Similar to Mergeable
, creating HasCachedRelationships.relatedCacheKeys
usually involves boilerplate, so I suggest using Sourcery to help automate the process with large numbers of models.
APIs often return related models in addition to the primary model which all need to be inserted in the Cache
during processing. However, doing each as a separate call to Cache
can have performance impacts since Cache
needs to do some work to ensure thread safety, not to mention all the observations that would be generated.
Instead, it can be useful to batch operations to a Cache
:
cache.performBatchUpdates { (cache) in
cache.set(...)
cache.set(...)
}
During a batch update, the closure is given a CacheType
, a slimmed down version of Cache
, which can be used to get and set values in the cache. The given CacheType
isn't safe to use across multiple threads, but that also makes it faster to use. Observations are coalesced and executed after the closure.
Zach Radke, [email protected]
Pancake is available under the MIT license. See the LICENSE file for more info.