All open-source packages
ArchitectureSwift

iOS-Offline-First-Framework

A local store for Swift apps that keeps working offline and syncs with your server.

2StarsLatest Release: v2.1.0 · Updated: October 21, 2026
Platforms:iOS 17macOS 14tvOS 17watchOS 10visionOS 1

License: MIT

Tests: 375Measured:

How it was measured

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.

Summary

  • A Swift package that keeps records in a file on the device, queues every change and syncs with any server when there is a connection.
  • Version 2.1.0 fixes the data-integrity findings of two audits of 2.0.0 and of their later rounds, each with a test that failed before the fix.
  • Only a push answer the store has read settles a change; after a lost or unreadable answer the change stays in doubt until its next push.
  • A new server epoch counts only when two requests of one sync report it, and the store never goes back to an earlier epoch.
  • Answers from another deployment behind the same URL are only partly covered; eight open issues describe each case and the way back.

A Swift package that keeps an app's records in a file on the device, queues every change and syncs with a server whenever there is a connection. Saving never waits for the network. When the same record changed on two devices, a conflict policy you choose decides what is kept. It works with any backend: a small HTTP protocol with a reference server, your own API through a two-function protocol, or an in-memory server for tests and previews. The package has no dependencies.

Features

  • Writes that never wait: save and delete go to an atomically written file and a durable queue of pending changes; failed pushes are retried with exponential backoff and jitter, and parked after the last attempt instead of being lost
  • Conflict policies: last write wins on hybrid logical clock timestamps, server wins, client wins, your own merge, or conflict-free fields (LWWRegister, LWWMap, GCounter, PNCounter, ORSet)
  • Any backend: HTTPSyncTransport speaks a documented two-endpoint JSON protocol that the Node reference server in Examples/SyncServer implements; any other API fits behind the SyncTransport protocol
  • SwiftUI: SyncedCollection is an @Observable model of a store; automatic sync runs when the device comes online and shortly after local edits
  • Data at rest: AES-GCM file encryption with a key kept in the Keychain, record schema migrations and a policy for damaged files
  • Bounded against misbehaving servers and damaged files: outbox size, pull pages, pushes per record, response, revision and change token sizes, and file names that cannot leave their directory
  • Stated guarantees: a write the server accepted is never replaced by a stale copy, only an answer that was read settles a change, and a new server epoch counts only when two requests of one sync report it; the Guarantees and limits article says what a server must do for that
  • Tested with Swift Testing: 375 tests on macOS and 372 on an iPhone simulator, including property tests that run every conflict policy through lost requests, restarts, token resets, stale pages and crashes, and end-to-end suites in CI against the reference server, which has 28 Node tests of its own

Quickstart

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]

Screenshots

The OfflineTodos example app on an iPhone simulator with every to-do synced, light mode
The OfflineTodos example app offline with two changes waiting to sync, light mode
The OfflineTodos example app with every to-do synced, dark mode
The OfflineTodos example app offline with two changes waiting to sync, dark mode

Timeline

  1. Repository opened as a set of manager classes; the sync manager simulated its results and never contacted a server

  2. Version 1.0.0 tagged; its manifest declared a missing Resources folder and unsafe compiler flags, so it could not be added as a dependency by version

  3. Trimmed down: RxSwift and CocoaLumberjack replaced with Combine, managers shortened, sync still a placeholder

  4. Rebuilt from scratch as an offline store with a sync engine and released as 2.0.0; the SwiftPersistence package was merged in and archived, no 1.0.0 API remains. Two audits of 2.0.0 the same day listed eleven must-fix data-integrity findings

  5. v2.1.0: data-integrity release after fifteen rounds of fixes and checks; rounds 14 and 15 were dropped after regressions, the release branch went back to the last commit of round 13, and eight known limitations were filed as issues

Links

Merged Repositories

Related Article

Read the article