A veces puede ser más conveniente no tratar con Datos centrales y solo para guardar el contenido de la memoria caché en el disco. Puede lograr esto con NSKeyedArchiver
y UserDefaults
(estoy usando Swift 3.0.2 en ejemplos de código a continuación).
primero vamos a abstraer de NSCache
e imaginamos que queremos ser capaces de persistir cualquier caché que se ajusta al protocolo:
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
tipo asociado tiene que ser Hashable
porque eso es requisito para el parámetro de tipo Set
.
A continuación tenemos que poner en práctica para NSCoding
Cache
usando clase de ayuda 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)
}
}
aquí:
C
es el tipo que se ajusta a Cache
.
C.Key
tipo asociado tiene que ajustarse a:
- Swift
CustomStringConvertible
protocolo para ser convertible a String
porque NSCoder.encode(forKey:)
método acepta String
para el parámetro clave.
- Swift
ExpressibleByStringLiteral
protocolo para convertir [String]
de nuevo a Set<Key>
- Necesitamos convertir
Set<Key>
a [String]
y almacenarla a NSCoder
con keys
clave, ya que no hay manera de extraer durante la decodificación de NSCoder
claves que se utilizaron cuando se codifica objetos. Pero puede haber una situación en la que también tenemos entrada en la memoria caché con la clave keys
, por lo que para distinguir las claves de caché de la clave especial keys
prefijimos las claves de caché con _
.
C.Value
tipo asociado tiene que ajustarse a NSCodingConvertible
protocolo para obtener NSCoding
casos de los valores almacenados en la memoria caché:
protocol NSCodingConvertible {
associatedtype Coding: NSCoding
var coding: Coding { get }
}
Value.Coding
tiene que ajustarse a ValueProvider
protocolo porque es necesario para obtener los valores de vuelta de NSCoding
casos:
protocol ValueProvider {
associatedtype Value
var value: Value { get }
}
C.Value.Coding.Value
y C.Value
tienen que ser equivalentes becau es el valor del que obtenemos la instancia NSCoding
cuando la codificación debe tener el mismo tipo que el valor que obtenemos de NSCoding
al decodificar.
CB
es un tipo que satisfaga Builder
protocolo y ayuda a crear la instancia de memoria caché de C
Tipo:
protocol Builder {
associatedtype Value
init()
func build() -> Value
}
A continuación vamos a hacer NSCache
se ajustan a Cache
protocolo. Aquí tenemos un problema. NSCache
tiene el mismo problema que NSCoder
, no proporciona la forma de extraer claves para objetos almacenados. Hay tres maneras de solucionar este:
Wrap NSCache
con el tipo de costumbre que mantiene claves Set
y usarlo en todas partes en lugar de 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)
}
}
Si usted todavía tiene que pasar NSCache
algún lugar entonces puede intentar extenderlo en Objective-C haciendo lo mismo que hice anteriormente con BetterCache
.
Utilice alguna otra implementación de caché.
Ahora usted tiene el tipo que se ajusta al protocolo Cache
y ya está listo para usarlo.
Vamos a definir el tipo Book
qué instancias vamos a almacenar en caché y NSCoding
para ese tipo:
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)
}
}
Algunos typealiases para una mejor legibilidad:
typealias BookCache = BetterCache<StringKey, Book>
typealias BookCacheCoding = CacheCoding<BookCache, BookCacheBuilder>
y constructor que nos ayudará a crear una instancia de Cache
instancia:
class BookCacheBuilder: Builder {
required init() {
}
func build() -> BookCache {
return BookCache()
}
}
prueba que:
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)
}
}
He utilizado Core Data en casi todas las aplicaciones de bases de datos que he creado, pero parece que esta no es la mejor opción. Estoy usando los cachés para almacenar los resultados de la API, manteniendo la aplicación en estado activo durante el lanzamiento y al cargar elementos que ya se han cargado. Me preocupa que la base de datos central se salga de control y siga creciendo. Simplemente no parece que los datos centrales se utilicen mejor para el almacenamiento en caché de datos "temporales". La persistencia que necesito es básicamente una función secundaria de la funcionalidad de caché en memoria que necesito, y esa es la razón por la que me estaba inclinando hacia NSCache. –
Yah - Puedo ver tu enigma. NSCoding no es * tan * difícil de implementar, siempre se puede ir por ese camino. Entonces la pregunta es cuándo escribir/actualizar la versión persistente de la memoria caché. En mi experiencia, es bastante fácil ir por un camino que termina por reinventar la rueda de la persistencia con todas sus complejidades. Y, por supuesto, la aplicación de mejor rendimiento es la que se envía primero. ;) – bbum
@CoryImdieke, ahora estamos enfrentando la misma situación y estoy planeando usar NSCache. ¿Solo me pregunto qué solución elegiste? – Koolala