◆ Skill · IA Ingenieria MLOps

Persistencia Thread-Safe
con Swift Actors

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.

Swift 5.9 · iOS 17
actor model
@Observable ViewModels
Offline-first
✗ Antes — Legacy
DispatchQueue + NSLock
Sincronización manual. Data races detectados en XCTest. Código frágil, difícil de auditar.
✓ Ahora — Actor Model
LocalRepository<T> actor
Aislamiento garantizado por el compilador. Ningún data race posible. API async/await limpia.
1 Arquitectura de capas
FeedView
TemplateLibraryView
ProjectDetailView
SwiftUI Views
SessionViewModel
TemplateViewModel
@Observable ViewModels
LocalRepository<T>
actor — serializa accesos
Session
PromptTemplate
Codable & Identifiable
sessions.json
templates.json
Disco (.atomic write)
2 Repositorio genérico actor
LocalRepository.swift Swift 5.9
/// 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
    }
}
3 ViewModel @Observable + actor
SessionViewModel.swift Swift 5.9
@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() }
    }
}
4 Tests de concurrencia (async let + TaskGroup)
Test 1
Escrituras concurrentes
3 saves simultáneos con async let. El actor serializa el acceso: cada write espera al anterior sin bloquear el hilo.
✓ 3 sesiones — sin data race
Test 2
Lecturas concurrentes
4 finds simultáneos con TaskGroup. Lecturas O(1) desde la caché en memoria, totalmente concurrentes.
✓ 3/4 IDs encontrados
Test 3
Eliminaciones concurrentes
2 deletes simultáneos con async let. Queda exactamente 1 ítem — conteo correcto, sin corrupción.
✓ 1 sesión restante
5 Decisiones de diseño
actor vs NSLock
El compilador verifica el aislamiento en tiempo de compilación. Es imposible olvidar un lock o desbloquearlo en el orden incorrecto.
💾
Caché + fichero
Lecturas O(1) desde el diccionario en memoria. Escrituras atómicas al disco con .atomic para prevenir corrupción en crashes.
🛠
init() síncrono
Cargar el fichero de forma síncrona en init evita la complejidad de async initializers, con coste mínimo para ficheros locales.
🔄
Genérico sobre T
Un solo LocalRepository<T> sirve para Session, PromptTemplate o cualquier modelo futuro. Sin duplicar código.
👀
@Observable ViewModel
Combinar el actor con @Observable permite que SwiftUI re-renderice automáticamente cuando cambian los datos.
🔒
Sendable en modelos
Los modelos conforman Sendable para que el compilador verifique que no se pasan referencias mutables entre actores.
6 Datos de ejemplo — sesiones del coach
Luna Ramos — Growth Agency
Auditoría de contenido con IA
Revisamos pipeline de creación de contenido. 3 puntos de fricción: brief, revisión, distribución. Propuesta: automatizar con agentes de Claude.
contenido automatización agentes
Pedro Salas — Ecommerce Moda
Segmentación de audiencias y prompts personalizados
40k emails, 5 segmentos definidos. Plantillas de prompts por segmento. Próximo paso: A/B test con Klaviyo.
email segmentación ecommerce
Ana Cortés — SaaS B2B
Onboarding con IA conversacional
7 pasos mapeados. Asistente contextual con Claude para reducir time-to-value de 14 días a 3.
onboarding conversacional saas
Marcos Ibáñez — Consultoría RRHH
Automatización de informes de candidatos
De 45 min/informe a 5 min. Extracción estructurada con prompt engineering + plantilla de informe ejecutivo.
rrhh automatización informes