Tous les paquets open source
ArchitectureSwift

iOS-Offline-First-Framework

Un store local pour apps Swift qui continue de fonctionner hors ligne et se synchronise avec votre serveur.

2ÉtoilesDernière version: v2.1.0 · Mis à jour: 21 octobre 2026
Plateformes:iOS 17macOS 14tvOS 17watchOS 10visionOS 1

Licence: MIT

Tests: 375Mesuré le:

Méthode de mesure

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.

Résumé

  • Un package Swift qui garde les enregistrements dans un fichier sur l'appareil, met chaque changement en file d'attente et se synchronise avec tout serveur s'il y a une connexion.
  • La version 2.1.0 corrige les constats de data integrity de deux audits de 2.0.0 et de leurs rounds suivants, chacun avec un test qui échouait avant le fix.
  • Seule une réponse de push que le store a lue rend un changement définitif ; après une réponse perdue ou illisible, le changement reste en doute jusqu'à son prochain push.
  • Une nouvelle server epoch ne compte que lorsque deux requests d'un même sync la rapportent, et le store ne revient jamais à une epoch antérieure.
  • Les réponses d'un autre deployment derrière la même URL ne sont couvertes que partiellement ; huit issues ouvertes décrivent chaque cas et le chemin de retour.

Un package Swift qui conserve les enregistrements d'une app dans un fichier sur l'appareil, met chaque changement en file d'attente et se synchronise avec un serveur dès qu'il y a une connexion. L'enregistrement n'attend jamais le réseau. Quand le même enregistrement a changé sur deux appareils, une conflict policy que vous choisissez décide ce qui est conservé. Il fonctionne avec n'importe quel backend : un petit protocole HTTP avec un serveur de référence, votre propre API via un protocol à deux fonctions, ou un serveur en mémoire pour les tests et les previews. Le package n'a aucune dépendance.

Fonctionnalités

  • Des écritures qui n'attendent jamais : save et delete vont dans un fichier écrit de façon atomique et une file d'attente durable de changements en attente ; les pushes échoués sont retentés avec un exponential backoff et du jitter, et mis de côté après la dernière tentative plutôt que perdus
  • Conflict policies : last write wins sur des timestamps de hybrid logical clock, server wins, client wins, votre propre merge, ou des champs sans conflit (LWWRegister, LWWMap, GCounter, PNCounter, ORSet)
  • N'importe quel backend : HTTPSyncTransport parle un protocole JSON documenté à deux endpoints, que le serveur de référence Node dans Examples/SyncServer implémente ; toute autre API se place derrière le protocol SyncTransport
  • SwiftUI : SyncedCollection est un modèle @Observable d'un store ; la synchronisation automatique s'exécute quand l'appareil revient en ligne et peu après des modifications locales
  • Données au repos : chiffrement de fichiers AES-GCM avec une clé conservée dans le Keychain, des migrations du schema des enregistrements et une policy pour les fichiers endommagés
  • Borné face aux serveurs qui se comportent mal et aux fichiers endommagés : taille de l'outbox, pages de pull, pushes par enregistrement, tailles de response, revision et change token, et noms de fichiers qui ne peuvent pas sortir de leur répertoire
  • Garanties énoncées : un write que le serveur a accepté n'est jamais remplacé par une copie périmée, seule une réponse qui a été lue rend un changement définitif, et une nouvelle server epoch ne compte que lorsque deux requests d'un même sync la rapportent ; l'article Guarantees and limits décrit ce qu'un serveur doit faire pour cela
  • Testé avec Swift Testing : 375 tests sur macOS et 372 sur un simulateur iPhone, dont des property tests qui font passer chaque conflict policy par des requests perdues, des redémarrages, des resets de token, des pages périmées et des crashes, et des suites end-to-end en CI face au serveur de référence, qui a ses propres 28 tests Node

Démarrage rapide

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}

Installation

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]

Captures d'écran

L'app d'exemple OfflineTodos sur un simulateur iPhone, toutes les tâches synchronisées, mode clair
L'app d'exemple OfflineTodos hors ligne, deux changements en attente de sync, mode clair
L'app d'exemple OfflineTodos avec toutes les tâches synchronisées, mode sombre
L'app d'exemple OfflineTodos hors ligne, deux changements en attente de sync, mode sombre

Chronologie

  1. Dépôt ouvert comme un ensemble de classes manager ; le manager de sync simulait ses résultats et ne contactait jamais de serveur

  2. Version 1.0.0 taguée ; son manifest déclarait un dossier Resources manquant et des flags de compilateur unsafe, si bien qu'elle ne pouvait pas être ajoutée comme dependency par version

  3. Allégé : RxSwift et CocoaLumberjack remplacés par Combine, managers raccourcis, sync toujours un placeholder

  4. Reconstruit de zéro comme un offline store avec un sync engine et publié sous 2.0.0 ; le package SwiftPersistence a été fusionné puis archivé, aucune API de la 1.0.0 ne subsiste. Deux audits de 2.0.0 le même jour ont listé onze constats de data integrity à corriger obligatoirement

  5. v2.1.0 : release de data integrity après quinze rounds de fixes et de checks ; les rounds 14 et 15 ont été abandonnés après des régressions, la release branch est revenue au dernier commit du round 13, et huit limitations connues ont été déposées comme issues

Liens

Dépôts fusionnés

Article associé

Lire l'article