### Initialize CKSyncEngine App Entry Point Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Sets up the main application entry point, initializing the SyncedDatabase with default settings for automatic syncing. ```swift import CloudKit import SwiftUI // App entry point — default automatic syncing @main struct SyncEngineApp: App { // Uses default data URL and automaticallySync = true let database = SyncedDatabase() var body: some Scene { WindowGroup { NavigationStack { ContactsList() .environmentObject(database) } } } } ``` -------------------------------- ### Initialize CKSyncEngine for Testing Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Provides a manual initialization method for SyncedDatabase, disabling automatic syncing and using a temporary URL for deterministic unit testing. ```swift // Manual init for tests — each instance acts as a separate "device" func newTestDatabase() -> SyncedDatabase { let dataURL = FileManager.default .temporaryDirectory .appending(component: "Contacts-\(UUID().uuidString)") .appendingPathExtension("json") return SyncedDatabase(automaticallySync: false, dataURL: dataURL) } ``` -------------------------------- ### Create a new test database instance Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Helper function to create a `SyncedDatabase` instance for testing. Each instance uses a unique temporary file URL and is configured with `automaticallySync: false` to allow manual control over synchronization. ```swift func newTestDatabase() -> SyncedDatabase { let url = FileManager.default.temporaryDirectory .appending(component: "Contacts-\(UUID().uuidString)") .appendingPathExtension("json") return SyncedDatabase(automaticallySync: false, dataURL: url) } ``` -------------------------------- ### Set up clean server zone for tests Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Deletes the server zone before each test to ensure a clean state for testing synchronization logic. ```swift override func setUp() async throws { // Delete server zone so every test starts clean let zoneID = CKRecordZone.ID(zoneName: Contact.zoneName) try await SyncedDatabase.container.privateCloudDatabase.deleteRecordZone(withID: zoneID) } ``` -------------------------------- ### Internal CKSyncEngine Initialization Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Configures and initializes the CKSyncEngine instance, specifying the database, state serialization for resuming, and the delegate. Background syncing is controlled by the `automaticallySync` property. ```swift // Internal initialization of CKSyncEngine func initializeSyncEngine() { var configuration = CKSyncEngine.Configuration( database: SyncedDatabase.container.privateCloudDatabase, stateSerialization: appData.stateSerialization, // resume from last known state delegate: self ) configuration.automaticallySync = automaticallySync _syncEngine = CKSyncEngine(configuration) } ``` -------------------------------- ### nextRecordZoneChangeBatch(_:syncEngine:) Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Provides the sync engine with batches of pending record changes to upload. ```APIDOC ## `nextRecordZoneChangeBatch(_:syncEngine:)` — Supplying Records for Upload Called by the sync engine when it is ready to upload pending record saves. Returns a `RecordZoneChangeBatch` built from the current in-memory contacts. If a pending save no longer has a corresponding local contact (e.g., deleted before upload completed), the pending change is removed from state rather than sending a nil record. ### Signature: ```swift func nextRecordZoneChangeBatch( _ context: CKSyncEngine.SendChangesContext, syncEngine: CKSyncEngine ) async -> CKSyncEngine.RecordZoneChangeBatch? ``` ### Description: This method is invoked by the `CKSyncEngine` to retrieve the next set of changes that need to be uploaded to CloudKit. It filters pending changes based on the provided scope and constructs a `RecordZoneChangeBatch`. For each pending change, it attempts to retrieve the corresponding local contact. If a contact has been deleted locally, the pending change is removed to avoid errors. Otherwise, it prepares a `CKRecord` for saving, reusing the last known server record if available to minimize conflicts. ``` -------------------------------- ### handleEvent(_:syncEngine:) Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Central method for handling all CloudKit sync engine events, dispatching them to specific handlers. ```APIDOC ## `handleEvent(_:syncEngine:)` — CKSyncEngineDelegate Event Handling The central `CKSyncEngineDelegate` method dispatches all sync lifecycle events. Each case maps to a focused handler; unknown events are logged rather than crashing, following the `@unknown default` pattern required for future-proofing against new CloudKit event types. ### Signature: ```swift func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async ``` ### Description: This method acts as the primary entry point for receiving and processing events from the `CKSyncEngine`. It uses a `switch` statement to differentiate between various event types such as state updates, account changes, fetched database or record zone changes, and sent record zone changes. Each recognized event is delegated to a specific handler function. Unrecognized events are logged to prevent application crashes. ``` -------------------------------- ### Test contact synchronization between two devices Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Simulates saving contacts on one device, sending changes, and then fetching them on another device to verify synchronization. Asserts that the second device initially has no contacts and then receives all contacts from the first device. ```swift func testSyncContacts(count: Int) async throws { let deviceA = newTestDatabase() let deviceB = newTestDatabase() let contacts = (0.. CKSyncEngine.RecordZoneChangeBatch? { let scope = context.options.scope let changes = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) } let contacts = appData.contacts return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: changes) { recordID in if let contact = contacts[recordID.recordName] { // Reuse the last known server record to minimise CloudKit conflict chances let record = contact.lastKnownRecord ?? CKRecord(recordType: Contact.recordType, recordID: recordID) contact.populateRecord(record) // writes encrypted name + userModificationDate return record } else { // Contact was deleted locally before upload — remove the stale pending change syncEngine.state.remove(pendingRecordZoneChanges: [.saveRecord(recordID)]) return nil } } } ``` -------------------------------- ### handleAccountChange(_:) Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Responds to iCloud account sign-in, sign-out, and account-switch events. It handles re-associating local data with a new account upon sign-in and wiping local data on sign-out or account switch. ```APIDOC ## `handleAccountChange(_:)` — iCloud Account Transitions Responds to sign-in, sign-out, and account-switch events. On sign-out or account switch the local data store is wiped. On sign-in all local contacts are re-queued for upload so they are associated with the new account. The sync engine is also re-initialised on local data deletion to flush stale state. ```swift func handleAccountChange(_ event: CKSyncEngine.Event.AccountChange) { switch event.changeType { case .signIn: // Re-upload existing local data to the newly signed-in account let recordZoneChanges = appData.contacts.values.map { CKSyncEngine.PendingRecordZoneChange.saveRecord($0.recordID) } syncEngine.state.add(pendingDatabaseChanges: [.saveZone(CKRecordZone(zoneName: Contact.zoneName))]) syncEngine.state.add(pendingRecordZoneChanges: recordZoneChanges) case .switchAccounts, .signOut: try? deleteLocalData() // wipes AppData and re-initializes sync engine @unknown default: Logger.database.log("Unknown account change: \(event)") } } ``` ``` -------------------------------- ### deleteLocalData() / deleteServerData() Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Provides methods to completely wipe local data (`deleteLocalData`) or server data (`deleteServerData`). `deleteLocalData` resets the app's data store and re-initializes the sync engine, while `deleteServerData` queues a zone deletion to remove all records from CloudKit. ```APIDOC ## `deleteLocalData()` / `deleteServerData()` — Full Data Wipe `deleteLocalData()` resets the in-memory `AppData` to empty, writes the blank state to disk, and re-initialises the sync engine (clearing all pending state). `deleteServerData()` queues a zone deletion and calls `sendChanges()` to immediately push the deletion to CloudKit, removing all records on the server. ```swift // Wipe everything locally (e.g., on account sign-out) func deleteLocalData() throws { appData = AppData() // empty contacts + nil stateSerialization try persistLocalData() initializeSyncEngine() // fresh engine with no pending changes } // Wipe the server zone (and all records within it) immediately func deleteServerData() async throws { let zoneID = CKRecordZone.ID(zoneName: Contact.zoneName) syncEngine.state.add(pendingDatabaseChanges: [.deleteZone(zoneID)]) try await syncEngine.sendChanges() // explicit flush, does not wait for push notification } ``` ``` -------------------------------- ### Delete Contacts Locally and Queue Deletions Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Handles contact deletion from the UI (swipe-to-delete, delete key) and queues `.deleteRecord` changes for server propagation. Ensure `database` and `sortedContacts` are accessible. ```swift // Swipe-to-delete in the list .onDelete { indexSet in let idsToDelete = indexSet.map { sortedContacts[$0].id } Task { try? await database.deleteContacts(idsToDelete) } } ``` ```swift // macOS Delete key handler .onDeleteCommand { let contactIDs = selection.compactMap { Contact.contactID(from: $0) } Task { try? await database.deleteContacts(Array(contactIDs)) } } ``` ```swift // Internal implementation func deleteContacts(_ ids: [Contact.ID]) throws { let contacts = ids.compactMap { appData.contacts[$0] } for id in ids { appData.contacts[id] = nil } try persistLocalData() let pendingDeletions: [CKSyncEngine.PendingRecordZoneChange] = contacts.map { .deleteRecord($0.recordID) } syncEngine.state.add(pendingRecordZoneChanges: pendingDeletions) } ``` -------------------------------- ### Handle iCloud Account Changes Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Responds to iCloud account sign-in, sign-out, and switch events. On sign-out or account switch, local data is wiped. On sign-in, local contacts are re-queued for upload. ```swift func handleAccountChange(_ event: CKSyncEngine.Event.AccountChange) { switch event.changeType { case .signIn: // Re-upload existing local data to the newly signed-in account let recordZoneChanges = appData.contacts.values.map { CKSyncEngine.PendingRecordZoneChange.saveRecord($0.recordID) } syncEngine.state.add(pendingDatabaseChanges: [.saveZone(CKRecordZone(zoneName: Contact.zoneName))]) syncEngine.state.add(pendingRecordZoneChanges: recordZoneChanges) case .switchAccounts, .signOut: try? deleteLocalData() // wipes AppData and re-initializes sync engine @unknown default: Logger.database.log("Unknown account change: \(event)") } } ``` -------------------------------- ### Add New Contact Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Creates a new Contact object with the current date as the modification date and saves it to the database. This action queues a `.saveRecord` change for upload. ```swift // Add a new contact from the UI func addNewContact() { let contact = Contact(userModificationDate: .now) // name auto-generated with random emoji Task { try? await database.saveContacts([contact]) } } ``` -------------------------------- ### Handle Fetched Record Zone Changes in Swift Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Processes incoming modifications and deletions from CloudKit. Modifications are merged into local contacts, and deletions remove them. Changes are persisted after each batch. ```swift func handleFetchedRecordZoneChanges(_ event: CKSyncEngine.Event.FetchedRecordZoneChanges) { for modification in event.modifications { let record = modification.record let id = record.recordID.recordName var contact = appData.contacts[id] ?? Contact(id: id) contact.mergeFromServerRecord(record) // conflict resolution via userModificationDate contact.setLastKnownRecordIfNewer(record) // cache server system fields for next upload appData.contacts[id] = contact } for deletion in event.deletions { appData.contacts[deletion.recordID.recordName] = nil } if !event.modifications.isEmpty || !event.deletions.isEmpty { try? persistLocalData() } } ``` -------------------------------- ### Delete Server Data Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Queues a zone deletion and immediately pushes the deletion to CloudKit, removing all records on the server. This function requires an `async` context. ```swift func deleteServerData() async throws { let zoneID = CKRecordZone.ID(zoneName: Contact.zoneName) syncEngine.state.add(pendingDatabaseChanges: [.deleteZone(zoneID)]) try await syncEngine.sendChanges() // explicit flush, does not wait for push notification } ``` -------------------------------- ### Save Contacts to Local Store and Queue Uploads Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Persists one or more Contact objects to the local JSON store and registers `.saveRecord` pending changes with the sync engine. It preserves the `lastKnownRecord` to prevent CloudKit conflicts. ```swift // Internal implementation func saveContacts(_ contacts: [Contact]) throws { for var contact in contacts { // Preserve existing last known record to avoid CloudKit conflict on next upload if let existingRecord = appData.contacts[contact.id]?.lastKnownRecord { contact.setLastKnownRecordIfNewer(existingRecord) } appData.contacts[contact.id] = contact } try persistLocalData() // Queue pending uploads — sync engine will send them automatically (or on demand) let pendingSaves: [CKSyncEngine.PendingRecordZoneChange] = contacts.map { .saveRecord($0.recordID) } syncEngine.state.add(pendingRecordZoneChanges: pendingSaves) } ``` -------------------------------- ### Delete Local Data Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Resets the in-memory `AppData` to empty, writes the blank state to disk, and re-initializes the sync engine. Used for wiping local data, e.g., on account sign-out. ```swift func deleteLocalData() throws { appData = AppData() // empty contacts + nil stateSerialization try persistLocalData() initializeSyncEngine() // fresh engine with no pending changes } ``` -------------------------------- ### deleteContacts(_:) Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Removes contacts from the local store and queues deletions for server synchronization. ```APIDOC ## `deleteContacts(_:)` — Local Removal and Queuing Deletions Removes contacts from the in-memory store and JSON file, then queues `.deleteRecord` changes so the sync engine propagates the deletions to the server and other devices. ### Usage Examples: ```swift // Swipe-to-delete in the list .onDelete { indexSet in let idsToDelete = indexSet.map { sortedContacts[$0].id } Task { try? await database.deleteContacts(idsToDelete) } } // macOS Delete key handler .onDeleteCommand { let contactIDs = selection.compactMap { Contact.contactID(from: $0) } Task { try? await database.deleteContacts(Array(contactIDs)) } } ``` ### Internal Implementation Signature: ```swift func deleteContacts(_ ids: [Contact.ID]) throws ``` ### Description of Internal Implementation: This function handles the actual deletion of contacts from the application's data model. It removes contacts from the in-memory dictionary, persists the changes to a local file, and then creates pending deletion changes for the `CKSyncEngine` to process and upload to the server. ``` -------------------------------- ### Handle Sent Record Zone Changes in Swift Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Manages the results of upload batches, updating local records for successful saves and handling various error codes for failed saves. Errors like `.serverRecordChanged`, `.zoneNotFound`, and `.unknownItem` trigger specific recovery mechanisms. ```swift func handleSentRecordZoneChanges(_ event: CKSyncEngine.Event.SentRecordZoneChanges) { var newPendingRecordZoneChanges = [CKSyncEngine.PendingRecordZoneChange]() var newPendingDatabaseChanges = [CKSyncEngine.PendingDatabaseChange]() // Update cached server records for successful saves for savedRecord in event.savedRecords { let id = savedRecord.recordID.recordName if var contact = appData.contacts[id] { contact.setLastKnownRecordIfNewer(savedRecord) appData.contacts[id] = contact } } for failedRecordSave in event.failedRecordSaves { let contactID = failedRecordSave.record.recordID.recordName switch failedRecordSave.error.code { case .serverRecordChanged: // Merge server record and re-queue; userModificationDate decides the winner guard let serverRecord = failedRecordSave.error.serverRecord, var contact = appData.contacts[contactID] else { continue } contact.mergeFromServerRecord(serverRecord) contact.setLastKnownRecordIfNewer(serverRecord) appData.contacts[contactID] = contact newPendingRecordZoneChanges.append(.saveRecord(failedRecordSave.record.recordID)) case .zoneNotFound: // Zone was deleted externally — recreate it and retry the record save let zone = CKRecordZone(zoneID: failedRecordSave.record.recordID.zoneID) newPendingDatabaseChanges.append(.saveZone(zone)) newPendingRecordZoneChanges.append(.saveRecord(failedRecordSave.record.recordID)) appData.contacts[contactID]?.lastKnownRecord = nil case .unknownItem: // Record deleted externally — clear stale system fields and re-upload newPendingRecordZoneChanges.append(.saveRecord(failedRecordSave.record.recordID)) appData.contacts[contactID]?.lastKnownRecord = nil case .networkFailure, .networkUnavailable, .zoneBusy, .serviceUnavailable, .notAuthenticated, .operationCancelled: Logger.database.debug("Retryable error, sync engine will retry automatically") default: Logger.database.fault("Unknown error saving record: \(failedRecordSave.error)") } } syncEngine.state.add(pendingDatabaseChanges: newPendingDatabaseChanges) syncEngine.state.add(pendingRecordZoneChanges: newPendingRecordZoneChanges) try? persistLocalData() } ``` -------------------------------- ### Test conflict resolution with differing save orders Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Simulates a save conflict where both devices modify the same record. It verifies that the record with the newer `userModificationDate` prevails, regardless of which device saves its changes first. Expects `.serverRecordChanged` errors during intermediate sync attempts. ```swift func testSaveFailure_conflict(winnerSavesFirst: Bool) async throws { let deviceA = newTestDatabase() let deviceB = newTestDatabase() var contactA = Contact(name: "A1", userModificationDate: Date()) try await deviceA.saveContacts([contactA]) try await deviceA.syncEngine.sendChanges() try await deviceB.syncEngine.fetchChanges() // Both devices make local edits; DeviceA has the newer userModificationDate contactA.name = "A2"; contactA.userModificationDate = Date.now + 1 var contactB = (await deviceB.appData.contacts)[contactA.id]! contactB.name = "B1"; contactB.userModificationDate = Date.now try await deviceA.saveContacts([contactA]) try await deviceB.saveContacts([contactB]) // Resolve conflict — "A2" must win regardless of upload order if winnerSavesFirst { try await deviceA.syncEngine.sendChanges() try? await deviceB.syncEngine.sendChanges() // expect .serverRecordChanged try await deviceB.syncEngine.sendChanges() // re-upload after merge try await deviceA.syncEngine.fetchChanges() } else { try await deviceB.syncEngine.sendChanges() try? await deviceA.syncEngine.sendChanges() // expect .serverRecordChanged try await deviceA.syncEngine.sendChanges() // re-upload after merge try await deviceB.syncEngine.fetchChanges() } let finalA = await deviceA.appData.contacts[contactA.id]! let finalB = await deviceB.appData.contacts[contactA.id]! XCTAssertEqual(finalA.name, "A2") // winner always prevails XCTAssertEqual(finalB.name, "A2") } ``` -------------------------------- ### Contact.mergeFromServerRecord(_:) Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Merges a CloudKit record into a local `Contact` object, using the `userModificationDate` as the truth signal to resolve conflicts. It prioritizes the record with the newer modification date. ```APIDOC ## `Contact.mergeFromServerRecord(_:)` — Conflict Resolution Merges a CloudKit record into a local `Contact` using the stored `userModificationDate` encrypted field as the truth signal. If the record from the server has a newer user modification date, the local name and date are overwritten; otherwise the local value is kept. This correctly resolves the "stale uploader wins" problem where the device with the older edit has a later network connection. ```swift // Example: DeviceA edited at t=1 (no network), DeviceB edited at t=2 (uploaded immediately). // When DeviceA finally uploads, it gets .serverRecordChanged and calls mergeFromServerRecord. mutating func mergeFromServerRecord(_ record: CKRecord) { let serverDate = record.encryptedValues[.contact_userModificationDate] as? Date ?? Date.distantPast if serverDate > userModificationDate { // Server wins — adopt remote name and date userModificationDate = serverDate if let name = record.encryptedValues[.contact_name] as? String { self.name = name } } // else: local wins — keep current values, sync engine will re-upload } // Populating a record before upload func populateRecord(_ record: CKRecord) { record.encryptedValues[.contact_name] = name record.encryptedValues[.contact_userModificationDate] = userModificationDate } ``` ``` -------------------------------- ### Rename Existing Contact Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Updates an existing contact's modification date and saves it to the database. This is necessary for conflict resolution and queues a `.saveRecord` change for upload. ```swift // Rename an existing contact after text field submission func onSubmit(contact: inout Contact) { contact.userModificationDate = Date() // required for conflict resolution Task { try? await database.saveContacts([contact]) } } ``` -------------------------------- ### Merge CloudKit Record into Local Contact Source: https://context7.com/apple/sample-cloudkit-sync-engine/llms.txt Merges a CloudKit record into a local Contact, using the `userModificationDate` as the truth signal to resolve conflicts. Overwrites local data if the server record is newer. ```swift mutating func mergeFromServerRecord(_ record: CKRecord) { let serverDate = record.encryptedValues[.contact_userModificationDate] as? Date ?? Date.distantPast if serverDate > userModificationDate { // Server wins — adopt remote name and date userModificationDate = serverDate if let name = record.encryptedValues[.contact_name] as? String { self.name = name } } // else: local wins — keep current values, sync engine will re-upload } ``` ```swift func populateRecord(_ record: CKRecord) { record.encryptedValues[.contact_name] = name record.encryptedValues[.contact_userModificationDate] = userModificationDate } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.