iOS- Offline- First- Framework
مخزن محلي لتطبيقات Swift يبقى يعمل دون اتصال ويتزامن مع خادمك.
الترخيص: 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 تحتفظ بالسجلات في ملف على الجهاز، وتضع كل تغيير في قائمة انتظار، وتتزامن مع أي خادم عند توفر اتصال.
- يصلح الإصدار 2.1.0 ملاحظات سلامة البيانات الناتجة عن تدقيقين لـ 2.0.0 وعن جولاتهما اللاحقة، ولكل إصلاح اختبار كان فاشلاً قبله.
- لا يُسوّى تغيير إلا بإجابة push قرأها store؛ وبعد إجابة مفقودة أو غير قابلة للقراءة يبقى التغيير مشكوكًا فيه حتى push التالي.
- لا يُحسب epoch جديد للخادم إلا إذا أفاد به طلبان من نفس sync، ولا يعود store أبدًا إلى epoch أقدم.
- الإجابات من deployment آخر خلف نفس URL مغطّاة جزئيًا فقط؛ وثمانية issues مفتوحة تصف كل حالة وطريق العودة.
حزمة Swift تحتفظ بسجلات التطبيق في ملف على الجهاز، وتضع كل تغيير في قائمة انتظار، وتتزامن مع الخادم كلما توفّر اتصال. الحفظ لا ينتظر الشبكة أبدًا. وإذا تغيّر السجل نفسه على جهازين، فإن conflict policy تختارها أنت تقرر ما يُحتفظ به. تعمل مع أي backend: بروتوكول HTTP صغير مع خادم مرجعي، أو API خاص بك عبر protocol من دالتين، أو خادم في الذاكرة للاختبارات والمعاينات. الحزمة بلا أي اعتماديات.
الميزات
- كتابات لا تنتظر: save وdelete يذهبان إلى ملف يُكتب بشكل atomic وقائمة انتظار دائمة من التغييرات المعلّقة؛ push الفاشل يُعاد تجربته بـ exponential backoff وjitter، ويُوضع جانبًا بعد المحاولة الأخيرة بدلاً من أن يُفقد
- سياسات التعارض: last write wins بالاعتماد على طوابع زمنية من hybrid logical clock، أو server wins، أو client wins، أو دمج (merge) خاص بك، أو حقول خالية من التعارض (LWWRegister وLWWMap وGCounter وPNCounter وORSet)
- أي backend: HTTPSyncTransport يتحدث بروتوكول JSON موثَّق بمسارين (endpoints) ينفّذه خادم Node المرجعي في Examples/SyncServer؛ وأي API آخر يوضع خلف protocol الخاص بـ SyncTransport
- SwiftUI: SyncedCollection هو نموذج @Observable لـ store؛ ويعمل sync التلقائي عندما يصبح الجهاز متصلاً وبعد التعديلات المحلية بوقت قصير
- البيانات عند التخزين: تشفير ملفات بـ AES-GCM بمفتاح محفوظ في Keychain، وmigrations لمخطط (schema) السجلات، وسياسة للملفات التالفة
- محدود في مواجهة الخوادم التي تسيء التصرف والملفات التالفة: حجم outbox، صفحات pull، عدد push لكل سجل، أحجام الاستجابة وrevision وchange token، وأسماء ملفات لا يمكنها الخروج من دليلها
- ضمانات مذكورة: الكتابة التي قبِلها الخادم لا تُستبدَل أبدًا بنسخة بائتة، ولا يُسوّى تغيير إلا بإجابة تمت قراءتها، ولا يُحسب epoch جديد للخادم إلا إذا أفاد به طلبان من نفس sync؛ ومقالة Guarantees and limits تشرح ما يجب أن يفعله الخادم لذلك
- مُختبَر بـ Swift Testing: 375 اختبارًا على macOS و372 على محاكي iPhone، منها property tests تُشغّل كل conflict policy عبر طلبات مفقودة وإعادة تشغيل وإعادة ضبط token وصفحات بائتة وانهيارات، وsuites من النوع end-to-end في CI مقابل الخادم المرجعي الذي له 28 اختبار Node خاصًا به
البدء السريع
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}التثبيت
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]لقطات الشاشة




الجدول الزمني
افتُتح المستودع كمجموعة من فئات (classes) manager؛ وكان sync manager يُحاكي نتائجه ولا يتصل بأي خادم أبدًا
وُسِم الإصدار 1.0.0 بوسم (tag)؛ وأعلن manifest عن مجلد Resources غير موجود وflags غير آمنة للمترجم، فلم يكن بالإمكان إضافته كـ dependency عبر رقم الإصدار
تبسيط: حلّ Combine محل RxSwift وCocoaLumberjack، واختُصرت الـmanagers، وبقي sync مجرد placeholder
أُعيد البناء من الصفر كـ offline store بمحرك sync وصدر كـ 2.0.0؛ ودُمجت حزمة SwiftPersistence فيه وأُرشِفت، ولم يتبق أي API من 1.0.0. وفي نفس اليوم أدرج تدقيقان (audits) لـ 2.0.0 أحد عشر ملاحظةً في سلامة البيانات (data integrity) يجب إصلاحها إلزاميًا
v2.1.0: إصدار لسلامة البيانات بعد خمس عشرة جولة من الإصلاحات والفحوصات؛ أُسقطت الجولتان 14 و15 بعد حدوث regressions، وعاد فرع (branch) الإصدار إلى آخر commit من الجولة 13، وفُتحت ثمانية قيود معروفة كـ issues