iOS- Offline- First- Framework
Un store local para apps Swift que sigue funcionando sin conexión y se sincroniza con tu servidor.
Licencia: MIT
Pruebas: 375Medido:
Cómo se midió
swift test at tag v2.1.0 (205c546) on macOS 27.0.1 with Swift 6.4: 375 tests in 75 suites passed; run again with OFFLINE_FIRST_NODE set so the three reference-server suites ran, with the same result; the two end-to-end suites that need OFFLINE_FIRST_SERVER_URL were skipped. npm test of Examples/SyncServer on Node 26.11.1: 28 tests passed. The README states 372 tests on an iPhone simulator, which was not re-run here.
Resumen
- Un paquete Swift que guarda los registros en un archivo en el dispositivo, encola cada cambio y se sincroniza con cualquier servidor cuando hay conexión.
- La versión 2.1.0 corrige los hallazgos de data integrity de dos audits de 2.0.0 y de sus rondas posteriores, cada uno con un test que fallaba antes del fix.
- Solo una respuesta de push que el store ha leído resuelve un cambio; tras una respuesta perdida o ilegible, el cambio queda en duda hasta su próximo push.
- Un nuevo server epoch solo cuenta cuando dos requests de un mismo sync lo informan, y el store nunca vuelve a un epoch anterior.
- Las respuestas de otro deployment detrás de la misma URL solo se cubren parcialmente; ocho issues abiertos describen cada caso y la vuelta atrás.
Un paquete Swift que guarda los registros de una app en un archivo en el dispositivo, encola cada cambio y se sincroniza con un servidor siempre que hay conexión. Guardar nunca espera a la red. Cuando el mismo registro cambió en dos dispositivos, una conflict policy que tú elijas decide qué se conserva. Funciona con cualquier backend: un pequeño protocolo HTTP con un servidor de referencia, tu propia API a través de un protocol de dos funciones, o un servidor en memoria para tests y previews. El paquete no tiene dependencias.
Características
- Escrituras que nunca esperan: save y delete van a un archivo escrito atómicamente y a una cola duradera de cambios pendientes; los pushes fallidos se reintentan con exponential backoff y jitter, y se aparcan tras el último intento en lugar de perderse
- Conflict policies: last write wins sobre timestamps de hybrid logical clock, server wins, client wins, tu propio merge, o campos libres de conflicto (LWWRegister, LWWMap, GCounter, PNCounter, ORSet)
- Cualquier backend: HTTPSyncTransport habla un protocolo JSON documentado de dos endpoints que implementa el servidor de referencia Node en Examples/SyncServer; cualquier otra API cabe detrás del protocol SyncTransport
- SwiftUI: SyncedCollection es un modelo @Observable de un store; la sincronización automática se ejecuta cuando el dispositivo se conecta y poco después de ediciones locales
- Datos en reposo: cifrado de archivos AES-GCM con una clave guardada en el Keychain, migraciones del schema de los registros y una policy para archivos dañados
- Acotado frente a servidores que se comportan mal y archivos dañados: tamaño del outbox, páginas de pull, pushes por registro, tamaños de response, revision y change token, y nombres de archivo que no pueden salir de su directorio
- Garantías declaradas: un write que el servidor aceptó nunca es reemplazado por una copia obsoleta, solo una respuesta que fue leída resuelve un cambio, y un nuevo server epoch solo cuenta cuando dos requests de un mismo sync lo informan; el artículo Guarantees and limits dice qué debe hacer un servidor para eso
- Probado con Swift Testing: 375 tests en macOS y 372 en un simulador de iPhone, incluyendo property tests que llevan cada conflict policy a través de requests perdidos, reinicios, resets de token, páginas obsoletas y crashes, y suites end-to-end en CI contra el servidor de referencia, que tiene sus propios 28 tests en Node
Inicio rápido
1import OfflineFirstFramework2import SwiftUI3 4/// A to-do item. Any `Codable` struct with an `id` can be a record.5struct Todo: SyncableRecord, Hashable {6 var id = UUID()7 var title: String8 var isDone = false9}10 11enum TodoStore {12 // InMemorySyncServer stands in for a backend; swap in HTTPSyncTransport or your own13 // SyncTransport when you have one. The initializer throws only for an invalid name or a14 // store file it cannot read.15 static let shared = try! OfflineStore<Todo>(name: "todos", transport: InMemorySyncServer<Todo>())16}17 18struct ContentView: View {19 @State private var todos = SyncedCollection(store: TodoStore.shared)20 @State private var newTitle = ""21 22 var body: some View {23 NavigationStack {24 List {25 TextField("New to-do", text: $newTitle)26 .onSubmit {27 let todo = Todo(title: newTitle)28 newTitle = ""29 Task { await todos.save(todo) }30 }31 ForEach(todos.records) { todo in32 Text(todo.title)33 }34 }35 .navigationTitle("To-dos")36 .toolbar {37 Text("\(todos.pendingCount) to sync")38 }39 .refreshable { await todos.sync() }40 }41 }42}Instalación
Xcode (File › Add Package Dependencies…)
1https://github.com/muhittincamdali/iOS-Offline-First-Framework.gitSwift Package Manager
1dependencies: [2 .package(url: "https://github.com/muhittincamdali/iOS-Offline-First-Framework.git", from: "2.1.0")3],4targets: [5 .target(name: "YourApp", dependencies: [6 .product(name: "OfflineFirstFramework", package: "iOS-Offline-First-Framework")7 ])8]Capturas de pantalla




Cronología
Repositorio abierto como un conjunto de clases manager; el manager de sync simulaba sus resultados y nunca contactaba a un servidor
Se etiquetó la versión 1.0.0; su manifest declaraba una carpeta Resources inexistente y flags de compilador unsafe, por lo que no podía añadirse como dependency por versión
Recortado: RxSwift y CocoaLumberjack sustituidos por Combine, los managers se acortaron, sync seguía siendo un placeholder
Reconstruido desde cero como un offline store con un sync engine y publicado como 2.0.0; el paquete SwiftPersistence se fusionó y se archivó, no queda ninguna API de la 1.0.0. Dos audits de 2.0.0 el mismo día listaron once hallazgos de data integrity de arreglo obligatorio
v2.1.0: release de data integrity tras quince rondas de fixes y checks; las rondas 14 y 15 se descartaron tras regresiones, la release branch volvió al último commit de la ronda 13, y se registraron ocho limitaciones conocidas como issues