所有开源包
架构Swift

iOS-Offline-First-Framework

一个让 Swift 应用离线也能继续工作、并与服务器同步的本地存储。

2星标数最新版本: v2.1.0 · 更新时间: 2026年10月21日
支持平台:iOS 17macOS 14tvOS 17watchOS 10visionOS 1

许可证: MIT

测试: 375测量日期:

测量方法

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.

摘要

  • 一个将记录保存在设备上文件中、把每次改动加入队列,并在有连接时与任意服务器同步的 Swift package。
  • 2.1.0 修复了 2.0.0 的两次审计及其后续各轮发现的 data-integrity 问题,每项修复都有一个此前会失败的测试。
  • 只有 store 读取过的 push 应答才会结算一次变更;应答丢失或无法读取后,该变更会保持未决,直到下一次 push。
  • 新的服务器 epoch 只有在同一次 sync 的两个请求都报告它时才会计数,且 store 绝不会回退到更早的 epoch。
  • 来自同一 URL 背后另一个 deployment 的应答只能被部分覆盖;八个未解决的 issue 描述了每种情况及恢复方式。

一个 Swift package,将应用的记录保存在设备上的一个文件中,把每一次改动加入队列,并在有连接时与服务器同步。保存操作从不等待网络。当同一条记录在两台设备上都发生变化时,由你选择的 conflict policy 决定保留哪一个。它适配任意 backend:带参考服务器的小型 HTTP 协议、通过双函数 protocol 对接的你自己的 API,或是用于测试与预览的内存服务器。该 package 没有任何依赖。

功能特性

  • 绝不等待的写入:save 和 delete 会写入一个以 atomic 方式写出的文件,以及一个持久化的待处理变更队列;失败的 push 会以 exponential backoff 和 jitter 重试,最后一次尝试后会被搁置,而不是丢失
  • 冲突策略(conflict policy):基于 hybrid logical clock 时间戳的 last write wins、server wins、client wins、你自己的 merge,或是无冲突字段(LWWRegister、LWWMap、GCounter、PNCounter、ORSet)
  • 适配任意 backend:HTTPSyncTransport 使用一个有文档说明的双 endpoint JSON 协议,由 Examples/SyncServer 中的 Node 参考服务器实现;其他任何 API 都可以置于 SyncTransport protocol 之后
  • SwiftUI:SyncedCollection 是 store 的一个 @Observable 模型;自动 sync 会在设备上线时以及本地编辑之后不久运行
  • 静态数据:使用保存在 Keychain 中的密钥进行 AES-GCM 文件加密、记录 schema 的 migration,以及针对损坏文件的策略
  • 针对异常服务器和损坏文件的边界:outbox 大小、pull 分页数、每条记录的 push 次数、response、revision 与 change token 的大小限制,以及不能脱离其目录的文件名
  • 明确声明的保证:服务器已接受的写入绝不会被一份过期的副本替换,只有被读取过的应答才会结算一次变更,新的服务器 epoch 只有在同一次 sync 的两个请求都报告它时才会计数;Guarantees and limits 一文说明了服务器为此必须做到什么
  • 使用 Swift Testing 测试:在 macOS 上有 375 个测试,在 iPhone 模拟器上有 372 个,其中包括让每种 conflict policy 都经历丢失请求、重启、token 重置、过期页面和崩溃的 property test,以及在 CI 中针对参考服务器运行的 end-to-end 测试套件,而该参考服务器自身有 28 个 Node 测试

快速开始

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}

安装

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]

截图

OfflineTodos 示例应用在 iPhone 模拟器上,所有待办事项均已同步,浅色模式
OfflineTodos 示例应用处于离线状态,有两项更改等待同步,浅色模式
OfflineTodos 示例应用,所有待办事项均已同步,深色模式
OfflineTodos 示例应用处于离线状态,有两项更改等待同步,深色模式

时间线

  1. 仓库以一组 manager 类的形式开放;sync manager 只是模拟结果,从未真正联系过服务器

  2. v1.0.0 被打上标签;其 manifest 声明了一个不存在的 Resources 文件夹和不安全的编译器 flags,因此无法按版本号添加为 dependency

  3. 精简:RxSwift 和 CocoaLumberjack 被 Combine 取代,manager 被缩短,sync 仍只是一个 placeholder

  4. 作为带有 sync engine 的离线存储从零重建,并以 2.0.0 发布;SwiftPersistence package 被合并进来并归档,1.0.0 的 API 一个都没有保留。同一天对 2.0.0 进行的两次审计列出了十一项必须修复的 data-integrity 问题

  5. v2.1.0:经过十五轮修复与检查后的 data-integrity 发布;第 14 和第 15 轮因出现 regression 被放弃,发布分支回退到第 13 轮的最后一次 commit,并将八项已知限制记录为 issue

相关链接

已合并的仓库

相关文章

阅读文章