From 0be92c833650321c3316fa5309dd5a3b14ba9afc Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 28 Oct 2025 20:45:31 -0500 Subject: [PATCH 1/4] Methods to explicitly fetch and send changes. --- .../xcshareddata/swiftpm/Package.resolved | 20 ++- Examples/Reminders/RemindersLists.swift | 5 + Sources/SQLiteData/CloudKit/SyncEngine.swift | 34 +++++ .../CloudKitTests/CloudKitTests.swift | 129 ++++++++++-------- 4 files changed, 128 insertions(+), 60 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0d54872..587d0c8d 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", + "originHash" : "41e7781e6c506773b6af84af513bcd6d3b1be59d635e6c4c4bd89638368e4629", "pins" : [ { "identity" : "combine-schedulers", @@ -73,6 +73,24 @@ "version" : "1.9.4" } }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", + "version" : "1.4.5" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, { "identity" : "swift-identified-collections", "kind" : "remoteSourceControl", diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 710aa677..0f7b49a0 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -304,6 +304,11 @@ struct RemindersListsView: View { .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) } } + .refreshable { + await withErrorReporting { + try await syncEngine.processChanges() + } + } .onAppear { model.onAppear() } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index b5368db2..d32294bd 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -503,6 +503,40 @@ } } + public func fetchChanges( + _ options: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions() + ) async throws { + let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { + ($0.private, $0.shared) + } + guard let privateSyncEngine, let sharedSyncEngine + else { return } + async let `private`: Void = privateSyncEngine.fetchChanges(options) + async let shared: Void = sharedSyncEngine.fetchChanges(options) + _ = try await (`private`, shared) + } + + public func sendChanges( + _ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() + ) async throws { + let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { + ($0.private, $0.shared) + } + guard let privateSyncEngine, let sharedSyncEngine + else { return } + async let `private`: Void = privateSyncEngine.sendChanges(options) + async let shared: Void = sharedSyncEngine.sendChanges(options) + _ = try await (`private`, shared) + } + + public func processChanges( + fetchOptions: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions(), + sendOptions: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() + ) async throws { + try await fetchChanges(fetchOptions) + try await sendChanges(sendOptions) + } + private func cacheUserTables(recordTypes: [RecordType]) async throws { try await userDatabase.write { db in try RecordType diff --git a/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift b/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift index e40c6499..169a1122 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift @@ -823,73 +823,84 @@ } } } - } - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func generatedColumns() async throws { - try await userDatabase.userWrite { db in - try db.seed { - ModelA(id: 1, count: 42, isEven: true) + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func sendChanges() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + } } + try await syncEngine.sendChanges(CKSyncEngine.SendChangesOptions()) } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: container, as: .customDump) { - """ - MockCloudContainer( - privateCloudDatabase: MockCloudDatabase( - databaseScope: .private, - storage: [ - [0]: CKRecord( - recordID: CKRecord.ID(1:modelAs/zone/__defaultOwner__), - recordType: "modelAs", - parent: nil, - share: nil, - count: 42, - id: 1 - ) - ] - ), - sharedCloudDatabase: MockCloudDatabase( - databaseScope: .shared, - storage: [] + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func generatedColumns() async throws { + try await userDatabase.userWrite { db in + try db.seed { + ModelA(id: 1, count: 42, isEven: true) + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:modelAs/zone/__defaultOwner__), + recordType: "modelAs", + parent: nil, + share: nil, + count: 42, + id: 1 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) ) - ) - """ - } + """ + } - let record = try syncEngine.private.database.record(for: ModelA.recordID(for: 1)) - record.encryptedValues["isEven"] = false - try await syncEngine.modifyRecords(scope: .private, saving: [record]).notify() - - assertInlineSnapshot(of: container, as: .customDump) { - """ - MockCloudContainer( - privateCloudDatabase: MockCloudDatabase( - databaseScope: .private, - storage: [ - [0]: CKRecord( - recordID: CKRecord.ID(1:modelAs/zone/__defaultOwner__), - recordType: "modelAs", - parent: nil, - share: nil, - count: 42, - id: 1, - isEven: 0 - ) - ] - ), - sharedCloudDatabase: MockCloudDatabase( - databaseScope: .shared, - storage: [] + let record = try syncEngine.private.database.record(for: ModelA.recordID(for: 1)) + record.encryptedValues["isEven"] = false + try await syncEngine.modifyRecords(scope: .private, saving: [record]).notify() + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:modelAs/zone/__defaultOwner__), + recordType: "modelAs", + parent: nil, + share: nil, + count: 42, + id: 1, + isEven: 0 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) ) - ) - """ - } + """ + } - try await userDatabase.read { db in - let modelA = try #require(try ModelA.find(1).fetchOne(db)) - #expect(modelA.isEven == true) + try await userDatabase.read { db in + let modelA = try #require(try ModelA.find(1).fetchOne(db)) + #expect(modelA.isEven == true) + } } } + } #endif From fb7b10992f0cddc72bf25c4f9139c56f7cb07e2c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 30 Oct 2025 11:18:38 -0500 Subject: [PATCH 2/4] docs --- Examples/Reminders/RemindersLists.swift | 2 +- Sources/SQLiteData/CloudKit/SyncEngine.swift | 34 +++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 0f7b49a0..971a8979 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -306,7 +306,7 @@ struct RemindersListsView: View { } .refreshable { await withErrorReporting { - try await syncEngine.processChanges() + try await syncEngine.syncChanges() } } .onAppear { diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index d32294bd..469158a7 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -502,7 +502,15 @@ } } } - + + /// Fetches pending remote changes from the server. + /// + /// Use this method to ensure the sync engine immediately fetches all pending remote changes + /// before your app continues. This isn’t necessary in normal use, as the engine automatically + /// syncs your app’s records. It is useful, however, in scenarios where you require more control + /// over sync, such as pull-to-refresh. + /// + /// - Parameter options: The options to use when fetching changes. public func fetchChanges( _ options: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions() ) async throws { @@ -515,7 +523,15 @@ async let shared: Void = sharedSyncEngine.fetchChanges(options) _ = try await (`private`, shared) } - + + /// Sends pending local changes to the server. + /// + /// Use this method to ensure the sync engine sends all pending local changes to the server + /// before your app continues. This isn’t necessary in normal use, as the engine automatically + /// syncs your app’s records. It is useful, however, in scenarios where you require greater + /// control over sync, such as a “Backup now” button. + /// + /// - Parameter options: The options to use when sending changes. public func sendChanges( _ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() ) async throws { @@ -528,8 +544,18 @@ async let shared: Void = sharedSyncEngine.sendChanges(options) _ = try await (`private`, shared) } - - public func processChanges( + + /// Synchronizes local and remote pending changes. + /// + /// Use this method to ensure the sync engine immediately fetches all pending remote changes + /// _and_ sends all pending local changes to the server. This isn't necessary in normal use, + /// as the engine autmoatically syncs your app's records. It is useful, however, in scenarios + /// where you require greater control over sync. + /// + /// - Parameters: + /// - fetchOptions: The options to use when fetching changes. + /// - sendOptions: The options to use when sending changes. + public func syncChanges( fetchOptions: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions(), sendOptions: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() ) async throws { From 174af788dd0077c7285340bedee784450b25519f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 30 Oct 2025 09:38:04 -0700 Subject: [PATCH 3/4] Fix typos in SyncEngine documentation comments Corrected typos in documentation comments for sync methods. --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 469158a7..40c022e2 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -506,7 +506,7 @@ /// Fetches pending remote changes from the server. /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes - /// before your app continues. This isn’t necessary in normal use, as the engine automatically + /// before your app continues. This isn't necessary in normal use, as the engine automatically /// syncs your app’s records. It is useful, however, in scenarios where you require more control /// over sync, such as pull-to-refresh. /// @@ -527,9 +527,9 @@ /// Sends pending local changes to the server. /// /// Use this method to ensure the sync engine sends all pending local changes to the server - /// before your app continues. This isn’t necessary in normal use, as the engine automatically + /// before your app continues. This isn't necessary in normal use, as the engine automatically /// syncs your app’s records. It is useful, however, in scenarios where you require greater - /// control over sync, such as a “Backup now” button. + /// control over sync, such as a "Backup now" button. /// /// - Parameter options: The options to use when sending changes. public func sendChanges( @@ -549,7 +549,7 @@ /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes /// _and_ sends all pending local changes to the server. This isn't necessary in normal use, - /// as the engine autmoatically syncs your app's records. It is useful, however, in scenarios + /// as the engine automatically syncs your app's records. It is useful, however, in scenarios /// where you require greater control over sync. /// /// - Parameters: From b02254413fce56af91fed19f534e328392f4fadf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 30 Oct 2025 09:38:31 -0700 Subject: [PATCH 4/4] Fix apostrophe formatting in documentation comments --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 40c022e2..76bbbb71 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -507,7 +507,7 @@ /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes /// before your app continues. This isn't necessary in normal use, as the engine automatically - /// syncs your app’s records. It is useful, however, in scenarios where you require more control + /// syncs your app's records. It is useful, however, in scenarios where you require more control /// over sync, such as pull-to-refresh. /// /// - Parameter options: The options to use when fetching changes. @@ -528,7 +528,7 @@ /// /// Use this method to ensure the sync engine sends all pending local changes to the server /// before your app continues. This isn't necessary in normal use, as the engine automatically - /// syncs your app’s records. It is useful, however, in scenarios where you require greater + /// syncs your app's records. It is useful, however, in scenarios where you require greater /// control over sync, such as a "Backup now" button. /// /// - Parameter options: The options to use when sending changes.