Czasami wygodniej jest nie zajmować się Core Data i tylko zapisać zawartość pamięci podręcznej na dysku. Możesz to osiągnąć dzięki NSKeyedArchiver
i UserDefaults
(używam Swift 3.0.2 w przykładach kodu poniżej).
Zacznijmy abstrahować od NSCache
i wyobrazić sobie, że chcemy, aby móc utrzymywać żadnych cache, który jest zgodny z protokołem:
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
związane typ musi być Hashable
bo to wymóg Set
typu parametru.
Następny musimy wdrożyć NSCoding
dla Cache
używając klasy pomocnika 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)
}
}
tutaj:
C
jest typ, który jest zgodny z Cache
.
C.Key
powiązanych rodzajów musi odpowiadać:
- Swift protokołu
CustomStringConvertible
być wymienialne na String
ponieważ NSCoder.encode(forKey:)
sposób przyjmuje String
dla kluczowych parametrów.
- Swift protokół
ExpressibleByStringLiteral
przekonwertować [String]
z powrotem do Set<Key>
- Musimy przekształcić
Set<Key>
do [String]
i przechowywać go do NSCoder
z keys
klucza, ponieważ nie ma sposobu, aby wyodrębnić podczas dekodowania z NSCoder
klucze, które były wykorzystywane podczas kodowania obiekty. Ale może się zdarzyć, że mamy także wpis w pamięci podręcznej z kluczem keys
, aby odróżnić klucze cache od specjalnego klucza keys
, przedrostek kluczy pamięci podręcznej z numerem _
.
C.Value
związane typ musi być zgodne z protokołem NSCodingConvertible
dostać NSCoding
przypadkach od wartości przechowywane w pamięci podręcznej:
protocol NSCodingConvertible {
associatedtype Coding: NSCoding
var coding: Coding { get }
}
Value.Coding
musi zgodne z protokołem ValueProvider
bo trzeba uzyskać wartości z powrotem NSCoding
przypadkach:
protocol ValueProvider {
associatedtype Value
var value: Value { get }
}
C.Value.Coding.Value
i C.Value
muszą być równoważne becau se wartość, z której otrzymujemy instancję NSCoding
, gdy kodowanie musi mieć ten sam typ co wartość, którą otrzymujemy z NSCoding
podczas dekodowania.
CB
to typ, który jest zgodny z protokołem Builder
i pomaga stworzyć instancję podręcznej C
typu:
protocol Builder {
associatedtype Value
init()
func build() -> Value
}
Następny zróbmy NSCache
zgodne z protokołem Cache
. Tutaj mamy problem. NSCache
ma ten sam problem co NSCoder
- nie zapewnia sposobu na wyodrębnienie kluczy dla przechowywanych obiektów. Istnieją trzy sposoby, aby obejść ten:
Wrap NSCache
z niestandardowego typu, który będzie posiadał klucze Set
i używać go wszędzie zamiast 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)
}
}
Jeśli trzeba jeszcze przejść NSCache
gdzieś potem możesz spróbować rozszerzyć go w Objective-C, robiąc to samo co powyżej z BetterCache
.
Użyj innej implementacji pamięci podręcznej.
Teraz masz typ, który jest zgodny z protokołem Cache
i jesteś gotowy, aby go używać.
Zdefiniujmy typ Book
który przypadki będziemy przechowywać w pamięci podręcznej i NSCoding
dla tego typu:
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)
}
}
Niektóre typealiases dla lepszej czytelności:
typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>
i budowniczym, które pomogą nam instancję Cache
instancja:
class BookCacheBuilder: Builder {
required init() {
}
func build() -> BookCache {
return BookCache()
}
}
Przetestuj:
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)
}
}
Używam Core Data na prawie każdej stworzonej przeze mnie bazie danych, ale wydaje mi się, że to nie jest najlepsze dopasowanie. Używam pamięci podręcznych do przechowywania wyników API, utrzymywania zippy aplikacji podczas uruchamiania i ładowania rzeczy, które zostały już załadowane. Martwię się, że główna baza danych wymyka się spod kontroli i rośnie. Po prostu nie wydaje się, że dane podstawowe są najlepiej wykorzystywane do buforowania "tymczasowych" danych. Wytrwałość, której potrzebuję, jest w zasadzie funkcją drugorzędną dla potrzebnej mi funkcji buforowania w pamięci, dlatego pochylałem się do NSCache. –
Yah - Widzę twoją zagadkę. NSCoding nie jest * trudny do wdrożenia - zawsze można przejść tą ścieżką. Wtedy pojawia się pytanie, kiedy napisać/zaktualizować trwałą wersję pamięci podręcznej. Z mojego doświadczenia wynika, że łatwo jest udać się na ścieżkę, która kończy się na ponownym wynalezieniu koła uporczywości ze wszystkimi jego złożonościami. Oczywiście najlepiej działającą aplikacją jest ta, która jest wysyłana jako pierwsza. ;) – bbum
@CoryImdieke, mamy teraz do czynienia z tą samą sytuacją i zamierzam użyć NSCache. Zastanawiasz się, które rozwiązanie wybrałeś? – Koolala