CocoaPods trunk is moving to be read-only. Read more on the blog, there are 14 months to go.
| 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.
CacheAny 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-01Creating the Mergeable implementations can be tedious with a large number of models, in which case Sourcery could be used.
HasCachedRelationshipsModels 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 UIThe 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.