{"id":31375942,"url":"https://github.com/orloff-n/takeoffkit","last_synced_at":"2025-09-28T02:52:11.298Z","repository":{"id":316500368,"uuid":"1063641797","full_name":"orloff-n/TakeoffKit","owner":"orloff-n","description":"A modern CloudKit sync engine for any local database (iOS 15+)","archived":false,"fork":false,"pushed_at":"2025-09-24T23:50:17.000Z","size":30,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-25T01:18:56.678Z","etag":null,"topics":["cloudkit","icloud-sync","ios","macos","swift","sync-engine"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/orloff-n.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-24T23:16:14.000Z","updated_at":"2025-09-24T23:50:21.000Z","dependencies_parsed_at":"2025-09-25T01:18:58.705Z","dependency_job_id":"9ad35e2a-3961-4bfe-b6ab-31ceec8f1277","html_url":"https://github.com/orloff-n/TakeoffKit","commit_stats":null,"previous_names":["orloff-n/takeoffkit"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/orloff-n/TakeoffKit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orloff-n%2FTakeoffKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orloff-n%2FTakeoffKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orloff-n%2FTakeoffKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orloff-n%2FTakeoffKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/orloff-n","download_url":"https://codeload.github.com/orloff-n/TakeoffKit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orloff-n%2FTakeoffKit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277318707,"owners_count":25798184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-28T02:00:08.834Z","response_time":79,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudkit","icloud-sync","ios","macos","swift","sync-engine"],"created_at":"2025-09-28T02:52:10.362Z","updated_at":"2025-09-28T02:52:11.286Z","avatar_url":"https://github.com/orloff-n.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🛫 TakeoffKit\n\n![Main branch build \u0026 test status](https://github.com/orloff-n/TakeoffKit/actions/workflows/swift.yml/badge.svg)\n\nTakeoffKit is a Swift library that simplifies synchronizing local data using CloudKit, abstracting away numerous CloudKit complexities like `CKRecord` conversion, rate limiting, error handling and many more. It provides a sync engine similar to Apple's [`CKSyncEngine`](https://developer.apple.com/documentation/cloudkit/cksyncengine-5sie5), but with more granular control and better backward compatibility.\n\nThe sync engine is designed to work with any local persistence framework – CoreData, SwiftData, Realm, etc.\n\n## Features\n### 🚀 Modern, yet compatible\nBuilt with modern async/await APIs and Swift 6 ready, but still offering iOS 15 compatibility.\n\n### ⚙️ Event-driven architecture\nDesigned as a [Mealy machine](https://en.wikipedia.org/wiki/Mealy_machine), the sync engine's state and actions are determined by a finite set of events, resulting in predictable and traceable behavior.\n\n### ✅ Robust error handling\nAutomatically handles recoverable CloudKit errors and retries failed operations. If a non-recoverable error occurs, the sync engine stops and notifies its delegate.\n\n### 🔎 Observable state\nThe sync engine's state properties can be monitored for better UI/UX or debugging.\n\n### 📦 Developer-friendly package\nNo external dependencies and comprehensive documentation.\n\n## Requirements\n- iOS 15.0+, macOS 12.0+, tvOS 15.0+, visionOS 1.0+, watchOS 8.0+\n- Swift 5.5+ (visionOS requires Swift 5.9+)\n\n## Installation\nAdd a dependency using Swift Package Manager:\n1. In Xcode select `File → Add Package Dependencies...`\n2. Enter the repository URL: `https://github.com/orloff-n/TakeoffKit.git`\n3. Select a dependency rule and add the package to your project\n\n## Usage\n### Prerequisite\nEnsure that your app is [configured to use CloudKit](https://developer.apple.com/documentation/cloudkit/enabling-cloudkit-in-your-app).\n\n### 1. Prepare your data models\nConform your data models to `TKSyncable` protocol. An example for Realm:\n```swift\nfinal class Folder: TKSyncable {\n    @Persisted(primaryKey: true) var id: ObjectId\n    @Persisted var index: Int\n    @Persisted var name: String\n    @Persisted(originProperty: \"folder\") var accounts: LinkingObjects\u003cAccount\u003e\n\n\n    // TKSyncable conformance:\n    @Persisted var tkMetadata: Data?\n    var tkRecordID: String { id.stringValue }\n    var tkProperties: [String: TKSyncableValue] { [\n        \"index\": .value(index),\n        \"name\": .encryptedValue(name)\n    ] }\n}\n```\n\n### 2. Initialize the sync engine\nCreate a configuration and initialize `TKSyncEngine` with it:\n```swift\nlet config = TKSyncEngineConfiguration(\n    containerID: \"iCloud.com.example.MyApp\",\n    zoneName: \"MyDataZone\",\n    subscriptionID: \"MySubscription\"\n)\n\nlet engine = TKSyncEngine(configuration: config)\n```\n\n### 3. Set a delegate\nConform one of your classes to `TKSyncEngineDelegate` and implement these methods:\n```swift\nextension YourDelegate: TKSyncEngineDelegate {\n    func syncEngine(_ engine: TKSyncEngine, didStopWithError error: any Error) {\n        // Handle errors\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didChangeAccountStatus accountStatus: CKAccountStatus) {\n        // React to account status changes\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didUpdateChangeToken changeToken: CKServerChangeToken?) {\n        // Persist the received token\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didFetchModifications modifications: [TKRecord]) {\n        // Persist changes\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didFetchDeletions deletions: [(recordID: String, recordType: String)]) {\n        // Persist changes\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, fetchDidFailFor failedIDs: [String : any Error]) {\n        // Handle per-record errors\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didSendModifications modifiedRecords: [TKRecord]) {\n        // Update local items (e.g. mark them as synced)\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, didSendDeletions deletedIDs: [String]) {\n        // Update local items (e.g. hard delete them)\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, sendDidFailFor failedIDs: [String : any Error]) {\n        // Handle per-record errors\n    }\n    \n    func syncEngine(_ engine: TKSyncEngine, shouldResolveConflict conflict: TKConflict) -\u003e CKRecord {\n        // Implement your conflict resolution logic. Example:\n        if let clientDate = conflict.clientRecord.modificationDate,\n           let serverDate = conflict.serverRecord.modificationDate {\n            return clientDate \u003e serverDate ? conflict.clientRecord : conflict.serverRecord\n        }\n        \n        return conflict.clientRecord\n    }\n}\n```\n\nAssign the sync engine's delegate:\n```swift\nengine.delegate = self\n```\n\n### 4. Start the sync engine\nCall `start()` on `TKSyncEngine` to start performing CloudKit operations:\n```swift\nengine.start() // Start syncing\n\n// Send changes\nengine.sendChanges(modify: modifiedItems, delete: deletedIDs)\n\n// Fetch changes\nengine.fetchChanges(token: lastChangeToken)\n```\n\n\u003e [!TIP]\n\u003e You can call `fetchChanges(token:)` and `sendChanges(modify:delete:)` at any time. These operations will be added to the queue, but they will not be performed unless the sync engine is running and the required conditions are met.\n\n### 5. Handle remote notifications\nFor real-time updates, register the app for remote notifications and handle them in `AppDelegate`:\n```swift\nfinal class AppDelegate: UIResponder, UIApplicationDelegate {\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -\u003e Bool {\n        application.registerForRemoteNotifications()\n        return true\n    }\n    \n    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -\u003e UIBackgroundFetchResult {\n        // Filter CloudKit notifications by subscriptionID (should match TKSyncEngineConfiguration)\n        if let notification = CKNotification(fromRemoteNotificationDictionary: userInfo),\n           notification.subscriptionID == \"MySubscription\" {\n            // Fetch changes and return the appropriate UIBackgroundFetchResult\n        }\n        \n        // Other notifications\n        return .noData\n    }\n}\n```\n\n## Limitations\n\u003e [!IMPORTANT]\n\u003e The following CloudKit features are not supported:\n\u003e - Public databases\n\u003e - Record sharing\n\u003e - Multiple record zones \u0026 subscriptions\n\n## Acknowledgments\nThe event-driven architecture and state management were heavily inspired by [CloudSyncSession](https://github.com/ryanashcraft/CloudSyncSession).\n\nAt first, I merely considered forking it just to update its deprecated CloudKit APIs. But the more I dug into its codebase, the more I felt the need to write my own implementation. Here's what I've done differently:\n- Built the entire engine with modern async/await syntax, replacing deprecated CloudKit APIs and ensuring full compatibility with Swift 6 strict concurrency mode\n- Added an abstraction layer for bidirectional conversion between local data models and CloudKit records, reducing boilerplate while maintaining flexibility\n- Removed the middleware pattern in favor of a straightforward private method call chain for improved logic clarity and code readability\n- Simplified event handling - there are fewer events, they contain less data and they are never replaced, which makes it much easier to trace the processing flow\n- Optimized queue management - operations remain in the queue until they finish successfully or are replaced with other operations, eliminating unnecessary state changes\n- Implemented a delegate pattern instead of Combine publishers for better convenience and easier integration\n\nYet, CloudSyncSession is one of the best CloudKit libraries available. Many thanks to Ryan Ashcraft for creating and open-sourcing such an excellent project.\n\n## License\nTakeoffKit is released under the MIT License. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forloff-n%2Ftakeoffkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forloff-n%2Ftakeoffkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forloff-n%2Ftakeoffkit/lists"}