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..971a8979 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.syncChanges() + } + } .onAppear { model.onAppear() } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index b5368db2..76bbbb71 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -502,6 +502,66 @@ } } } + + /// 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 { + 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) + } + + /// 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 { + 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) + } + + /// 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 automatically 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 { + try await fetchChanges(fetchOptions) + try await sendChanges(sendOptions) + } private func cacheUserTables(recordTypes: [RecordType]) async throws { try await userDatabase.write { db in 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