cat swift/caching-leggero-persistenza-locale.md
Caching leggero e persistenza locale
Cache in memoria, salvataggio JSON e scadenza dei dati: persistere non significa introdurre subito un database.
Non ogni dato richiede un database. Spesso serve una cache leggera:
- evitare richieste ripetute;
- supportare offline parziale;
- velocizzare avvio;
- ridurre uso rete;
- mantenere snapshot temporanei.
In Swift puoi partire da:
- actor cache in memoria;
- file JSON locale;
UserDefaultsper preferenze piccole;- database solo quando serve.
Cache in memoria
actor ProductCache {
private var storage: [String: Product] = [:]
func get(_ id: String) -> Product? {
storage[id]
}
func set(_ product: Product, for id: String) {
storage[id] = product
}
}
Actor per accesso concorrente sicuro.
Cache con scadenza
struct CacheEntry<Value: Sendable>: Sendable {
let value: Value
let insertedAt: Date
}
actor ExpiringCache<Value: Sendable> {
private var storage: [String: CacheEntry<Value>] = [:]
private let ttl: TimeInterval
init(ttl: TimeInterval) {
self.ttl = ttl
}
func get(_ key: String, now: Date = Date()) -> Value? {
guard let entry = storage[key] else {
return nil
}
guard now.timeIntervalSince(entry.insertedAt) < ttl else {
storage[key] = nil
return nil
}
return entry.value
}
func set(_ value: Value, for key: String, now: Date = Date()) {
storage[key] = CacheEntry(value: value, insertedAt: now)
}
}
Il tempo è iniettabile nei metodi per testare facilmente.
Persistenza su file
Per snapshot semplici:
struct ProductStore {
let fileURL: URL
let encoder: JSONEncoder
let decoder: JSONDecoder
func save(_ products: [Product]) throws {
let data = try encoder.encode(products)
try data.write(to: fileURL, options: [.atomic])
}
func load() throws -> [Product] {
let data = try Data(contentsOf: fileURL)
return try decoder.decode([Product].self, from: data)
}
}
atomic riduce il rischio di file parziali.
Gestire file mancante
func loadOrEmpty() throws -> [Product] {
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return []
}
return try load()
}
File mancante non è sempre errore.
UserDefaults
Usa UserDefaults per preferenze piccole:
- tema;
- flag onboarding;
- ultimo filtro;
- piccole impostazioni.
Non usarlo per cataloghi grandi o cache strutturate.
Cache policy
Devi decidere:
- quando leggere cache;
- quando invalidare;
- quando aggiornare in background;
- cosa fare se rete fallisce;
- cosa mostrare se cache è vecchia.
Strategia semplice:
- se cache valida, restituisci cache;
- altrimenti chiama API;
- salva nuovo valore;
- restituisci nuovo valore.
Repository con cache
struct ProductRepository {
let api: ProductAPI
let cache: ExpiringCache<Product>
func product(id: String) async throws -> Product {
if let cached = await cache.get(id) {
return cached
}
let product = try await api.product(id: id)
await cache.set(product, for: id)
return product
}
}
Questo incapsula la politica.
Checklist
Prima di persistere:
- è cache o fonte di verità?
- quanto dura il dato?
- cosa succede se è vecchio?
- come invalidi?
- cosa succede offline?
- il formato su disco può cambiare?
- il dato contiene informazioni sensibili?
- serve davvero un database?
Esercizio
Crea:
ExpiringCache<Product>;ProductStoresu file JSON;ProductRepositoryche usa API + cache.
Poi scrivi test manuali:
- cache hit;
- cache miss;
- cache scaduta;
- file mancante;
- decoding fallito.