Capa de persistencia local segura para CULTIVA IA Coach — app iOS offline-first para coaches de marketing con IA. Sin locks, sin DispatchQueue: seguridad garantizada por el compilador.
/// Repositorio actor-aislado: caché O(1) en memoria + volcado atómico a JSON en disco.
/// Thread-safety garantizada por el compilador — sin NSLock, sin DispatchQueue.
public actor LocalRepository<T: Codable & Identifiable & Sendable> where T.ID == String {
private var cache: [String: T] = [:]
private let fileURL: URL
public init(
directory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0],
filename: String = "data.json"
) {
self.fileURL = directory.appendingPathComponent(filename)
self.cache = Self.loadSynchronously(from: self.fileURL)
}
// MARK: — API Pública
public func save(_ item: T) throws {
cache[item.id] = item
try persistToFile()
}
public func delete(_ id: String) throws {
guard cache[id] != nil else { return }
cache[id] = nil
try persistToFile()
}
public func find(by id: String) -> T? { cache[id] } // O(1)
public func loadAll() -> [T] { Array(cache.values) }
// MARK: — Privado
private func persistToFile() throws {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(Array(cache.values))
try data.write(to: fileURL, options: .atomic) // .atomic previene corrupción
}
}
@Observable
final class SessionViewModel {
private(set) var sessions: [Session] = []
private let repository: LocalRepository<Session>
func load() async {
sessions = await repository.loadAll()
.sorted { $0.createdAt > $1.createdAt }
}
func add(clientName: String, topic: String) async throws {
let session = Session(clientName: clientName, topic: topic)
try await repository.save(session)
await load()
}
}
// Uso en SwiftUI — automáticamente reactivo con @Observable:
struct FeedView: View {
@State private var vm = SessionViewModel()
var body: some View {
List(vm.sessions) { session in
SessionRow(session: session)
}
.task { await vm.load() }
}
}
async let. El actor serializa el acceso: cada write espera al anterior sin bloquear el hilo.TaskGroup. Lecturas O(1) desde la caché en memoria, totalmente concurrentes.async let. Queda exactamente 1 ítem — conteo correcto, sin corrupción..atomic para prevenir corrupción en crashes.init evita la complejidad de async initializers, con coste mínimo para ficheros locales.LocalRepository<T> sirve para Session, PromptTemplate o cualquier modelo futuro. Sin duplicar código.@Observable permite que SwiftUI re-renderice automáticamente cuando cambian los datos.Sendable para que el compilador verifique que no se pasan referencias mutables entre actores.