Todos los paquetes de código abierto
ArquitecturaSwift

iOS-Offline-First-Framework

Un store local para apps Swift que sigue funcionando sin conexión y se sincroniza con tu servidor.

2EstrellasÚltima versión: v2.1.0 · Actualizado: 21 de octubre de 2026
Plataformas:iOS 17macOS 14tvOS 17watchOS 10visionOS 1

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

swift
1import OfflineFirstFramework
2import SwiftUI
3 
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: String
8 var isDone = false
9}
10 
11enum TodoStore {
12 // InMemorySyncServer stands in for a backend; swap in HTTPSyncTransport or your own
13 // SyncTransport when you have one. The initializer throws only for an invalid name or a
14 // 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 in
32 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…)

text
1https://github.com/muhittincamdali/iOS-Offline-First-Framework.git

Swift Package Manager

swift
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

La app de ejemplo OfflineTodos en un simulador de iPhone con todas las tareas sincronizadas, modo claro
La app de ejemplo OfflineTodos sin conexión, con dos cambios esperando a sincronizarse, modo claro
La app de ejemplo OfflineTodos con todas las tareas sincronizadas, modo oscuro
La app de ejemplo OfflineTodos sin conexión, con dos cambios esperando a sincronizarse, modo oscuro

Cronología

  1. Repositorio abierto como un conjunto de clases manager; el manager de sync simulaba sus resultados y nunca contactaba a un servidor

  2. 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

  3. Recortado: RxSwift y CocoaLumberjack sustituidos por Combine, los managers se acortaron, sync seguía siendo un placeholder

  4. 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

  5. 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

Enlaces

Repositorios fusionados

Artículo relacionado

Leer el artículo