Иногда может быть удобнее не иметь дело с Базовыми данными, а просто сохранять содержимое кэша на диск.Вы можете достичь этого с помощью NSKeyedArchiver
и UserDefaults
(я использую Swift 3.0.2 в примерах кода ниже).
Сначала давайте абстрагируемся от NSCache
и представим, что мы хотим быть в состоянии сохранениялюбой кэш, соответствующий протоколу:
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
связанный тип должен быть Hashable
, потому что это требование для параметра типа Set
.
Далее мы должны реализовать NSCoding
для Cache
с использованием вспомогательного класса 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)
}
}
Здесь:
C
- это тип, соответствующий Cache
. C.Key
связанный тип должен соответствовать: - протоколу Swift
CustomStringConvertible
, который должен быть преобразован в String
, поскольку метод NSCoder.encode(forKey:)
принимает String
в качестве ключевого параметра. - Swift
ExpressibleByStringLiteral
протокол для преобразования [String]
обратно в Set<Key>
- Нам нужно преобразовать
Set<Key>
в [String]
и сохранить его в NSCoder
с ключом keys
, поскольку при декодировании невозможно извлечь ключи NSCoder
, которые использовались при кодировании объектов.Но может быть ситуация, когда у нас также есть запись в кеше с ключом keys
, поэтому, чтобы отличить ключи кеша от специального ключа keys
, мы добавляем префиксы кеша с _
. C.Value
тип должен соответствовать протоколу NSCodingConvertible
, чтобы получить NSCoding
экземпляров из значений, хранящихся в кэше:
protocol NSCodingConvertible {
associatedtype Coding: NSCoding
var coding: Coding { get }
}
Value.Coding
должен соответствовать протоколу ValueProvider
, поскольку вынеобходимо получить значения обратно из NSCoding
экземпляров:
protocol ValueProvider {
associatedtype Value
var value: Value { get }
}
C.Value.Coding.Value
и C.Value
должны быть эквивалентными, поскольку значение, из которого мы получаем экземпляр NSCoding
при кодированиидолжен иметь тот же тип, что и значение, которое мы получаем из NSCoding
при декодировании.
CB
- это тип, который соответствует протоколу Builder
и помогает создать экземпляр кэшаC
тип:
protocol Builder {
associatedtype Value
init()
func build() -> Value
}
Далее давайте приведем NSCache
в соответствие с Cache
протоколом.Здесь у нас проблема.У NSCache
та же проблема, что и у NSCoder
- она не предоставляет способ извлечения ключей для хранимых объектов.Есть три способа обойти это:
Обернуть NSCache
пользовательским типом, который будет удерживать клавиши Set
и использовать его везде вместо 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)
}
}
Если вам все еще нужно передать куда-нибудь NSCache
, вы можете попытаться расширить его в Objective-C, выполнив то же самое, что я делал выше с BetterCache
.
Используйте некоторые другие реализации кэша.
Теперь у вас есть тип, соответствующий протоколу Cache
, и вы готовы его использовать.
Давайте определимвведите Book
экземпляры, которые мы будем хранить в кеше, и NSCoding
для этого типа:
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)
}
}
Некоторые шрифты для лучшей читабельности:
typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>
И конструктор, который поможет намэкземпляр Cache
экземпляр:
class BookCacheBuilder: Builder {
required init() {
}
func build() -> BookCache {
return BookCache()
}
}
проверить это:
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)
}
}