From 892472cef747f023bd1e0fe41b33a6961714f4e8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Sep 2025 12:54:08 -0700 Subject: [PATCH 1/5] Add observation to `SyncEngine.isRunning` --- Examples/Reminders/RemindersLists.swift | 4 ---- Sources/SQLiteData/CloudKit/SyncEngine.swift | 25 +++++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index ccde9af6..71303b51 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -192,7 +192,6 @@ class RemindersListsModel { struct RemindersListsView: View { @Bindable var model: RemindersListsModel - @State var id = UUID() @Dependency(\.defaultSyncEngine) var syncEngine var body: some View { @@ -319,13 +318,11 @@ struct RemindersListsView: View { Button { if syncEngine.isRunning { syncEngine.stop() - id = UUID() } else { Task { await withErrorReporting { try await syncEngine.start() } - id = UUID() } } } label: { @@ -387,7 +384,6 @@ struct RemindersListsView: View { .navigationDestination(item: $model.destination.detail) { detailModel in RemindersDetailView(model: detailModel) } - .id(id) } } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index f420334b..6ad08ddb 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -6,12 +6,13 @@ import GRDB import OrderedCollections import OSLog + import Observation import StructuredQueriesCore import SwiftData /// An object that manages the synchronization of local and remote SQLite data. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public final class SyncEngine: Sendable { + public final class SyncEngine: Observable, Sendable { package let userDatabase: UserDatabase package let logger: Logger package let metadatabase: any DatabaseWriter @@ -27,6 +28,7 @@ -> (private: any SyncEngineProtocol, shared: any SyncEngineProtocol) package let container: any CloudContainer let dataManager = Dependency(\.dataManager) + private let observationRegistrar = ObservationRegistrar() /// The error message used when a write occurs to a record for which the current user /// does not have permission. @@ -318,14 +320,17 @@ /// You must start the sync engine again using ``start()`` to synchronize the changes. public func stop() { guard isRunning else { return } - syncEngines.withValue { - $0 = SyncEngines() + observationRegistrar.withMutation(of: self, keyPath: \.isRunning) { + syncEngines.withValue { + $0 = SyncEngines() + } } } /// Determines if the sync engine is currently running or not. public var isRunning: Bool { - syncEngines.withValue { + observationRegistrar.access(self, keyPath: \.isRunning) + return syncEngines.withValue { $0.isRunning } } @@ -333,11 +338,13 @@ private func start() throws -> Task { guard !isRunning else { return Task {} } let (privateSyncEngine, sharedSyncEngine) = defaultSyncEngines(metadatabase, self) - syncEngines.withValue { - $0 = SyncEngines( - private: privateSyncEngine, - shared: sharedSyncEngine - ) + observationRegistrar.withMutation(of: self, keyPath: \.isRunning) { + syncEngines.withValue { + $0 = SyncEngines( + private: privateSyncEngine, + shared: sharedSyncEngine + ) + } } let previousRecordTypes = try metadatabase.read { db in From 1e0badefad1041120e55821fef04e12647c4558c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Sep 2025 13:05:44 -0700 Subject: [PATCH 2/5] wip --- Examples/Examples.xcodeproj/project.pbxproj | 37 ++++++++++--------- .../xcshareddata/swiftpm/Package.resolved | 2 +- Examples/RemindersTests/Reminders.xctestplan | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 2db7d786..a7d7bc4c 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -16,9 +16,9 @@ CA6A1D242E68A0A600604D6A /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = CA6A1D232E68A0A600604D6A /* SQLiteData */; }; CAD001872D874F1F00FA977A /* DependenciesTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CAD001862D874F1F00FA977A /* DependenciesTestSupport */; }; DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = DC5FA7472D4C63D60082743E /* DependenciesMacros */; }; + DC9A3DDE2E6A280700DE41FB /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = DC9A3DDD2E6A280700DE41FB /* SQLiteData */; }; + DC9A3DE02E6A280F00DE41FB /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = DC9A3DDF2E6A280F00DE41FB /* SQLiteData */; }; DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = DCBE8A132D4842BF0071F499 /* CasePaths */; }; - DCD9AC8B2E02176700FB20F8 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCD9AC8A2E02176700FB20F8 /* SharingGRDB */; }; - DCD9AC8F2E02177900FB20F8 /* SharingGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = DCD9AC8E2E02177900FB20F8 /* SharingGRDB */; }; DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = DCF267382D48437300B680BE /* SwiftUINavigation */; }; /* End PBXBuildFile section */ @@ -147,7 +147,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DCD9AC8B2E02176700FB20F8 /* SharingGRDB in Frameworks */, + DC9A3DDE2E6A280700DE41FB /* SQLiteData in Frameworks */, CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -175,7 +175,7 @@ files = ( DCF267392D48437300B680BE /* SwiftUINavigation in Frameworks */, DC5FA7482D4C63D60082743E /* DependenciesMacros in Frameworks */, - DCD9AC8F2E02177900FB20F8 /* SharingGRDB in Frameworks */, + DC9A3DE02E6A280F00DE41FB /* SQLiteData in Frameworks */, DCBE8A142D4842BF0071F499 /* CasePaths in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -289,7 +289,7 @@ name = CaseStudies; packageProductDependencies = ( CA2908C82D4AF70E003F165F /* UIKitNavigation */, - DCD9AC8A2E02176700FB20F8 /* SharingGRDB */, + DC9A3DDD2E6A280700DE41FB /* SQLiteData */, ); productName = Examples; productReference = CAF836982D4735620047AEB5 /* CaseStudies.app */; @@ -363,7 +363,7 @@ DCBE8A132D4842BF0071F499 /* CasePaths */, DCF267382D48437300B680BE /* SwiftUINavigation */, DC5FA7472D4C63D60082743E /* DependenciesMacros */, - DCD9AC8E2E02177900FB20F8 /* SharingGRDB */, + DC9A3DDF2E6A280F00DE41FB /* SQLiteData */, ); productName = SyncUps; productReference = DCBE89CC2D483FB90071F499 /* SyncUps.app */; @@ -416,7 +416,7 @@ DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */, DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */, CA5E47052DECEF0F0069E0F8 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, - DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference "../../sharing-grdb" */, + DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference ".." */, ); preferredProjectObjectVersion = 77; productRefGroup = CAF836992D4735620047AEB5 /* Products */; @@ -1004,9 +1004,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference "../../sharing-grdb" */ = { + DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference ".." */ = { isa = XCLocalSwiftPackageReference; - relativePath = "../../sharing-grdb"; + relativePath = ".."; }; /* End XCLocalSwiftPackageReference section */ @@ -1078,7 +1078,7 @@ }; CA6A1D232E68A0A600604D6A /* SQLiteData */ = { isa = XCSwiftPackageProductDependency; - package = DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference "../../sharing-grdb" */; + package = DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference ".." */; productName = SQLiteData; }; CAD001862D874F1F00FA977A /* DependenciesTestSupport */ = { @@ -1091,19 +1091,20 @@ package = DC5FA7462D4C63D60082743E /* XCRemoteSwiftPackageReference "swift-dependencies" */; productName = DependenciesMacros; }; - DCBE8A132D4842BF0071F499 /* CasePaths */ = { + DC9A3DDD2E6A280700DE41FB /* SQLiteData */ = { isa = XCSwiftPackageProductDependency; - package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; - productName = CasePaths; + package = DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference ".." */; + productName = SQLiteData; }; - DCD9AC8A2E02176700FB20F8 /* SharingGRDB */ = { + DC9A3DDF2E6A280F00DE41FB /* SQLiteData */ = { isa = XCSwiftPackageProductDependency; - productName = SharingGRDB; + package = DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference ".." */; + productName = SQLiteData; }; - DCD9AC8E2E02177900FB20F8 /* SharingGRDB */ = { + DCBE8A132D4842BF0071F499 /* CasePaths */ = { isa = XCSwiftPackageProductDependency; - package = DCD9AC892E02176700FB20F8 /* XCLocalSwiftPackageReference "../../sharing-grdb" */; - productName = SharingGRDB; + package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePaths; }; DCF267382D48437300B680BE /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d7571bf..ce14234a 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" : "22fb924569f92610b5675a628f98b8864244fe7f2f1702deb956f693c2598118", + "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", "pins" : [ { "identity" : "combine-schedulers", diff --git a/Examples/RemindersTests/Reminders.xctestplan b/Examples/RemindersTests/Reminders.xctestplan index 18ccddb6..8f339faf 100644 --- a/Examples/RemindersTests/Reminders.xctestplan +++ b/Examples/RemindersTests/Reminders.xctestplan @@ -20,7 +20,7 @@ "parallelizable" : true, "target" : { "containerPath" : "container:Examples.xcodeproj", - "identifier" : "CA9F99472DF9134D00934431", + "identifier" : "CA5E46952DEBFE410069E0F8", "name" : "RemindersTests" } } From 770a13074bcd235341305d780e180be5d32d15cf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Sep 2025 13:55:42 -0700 Subject: [PATCH 3/5] wip --- .../xcshareddata/swiftpm/Package.resolved | 4 +-- Examples/Reminders/RemindersApp.swift | 6 ++-- Examples/Reminders/Schema.swift | 35 +++++++++---------- Examples/RemindersTests/Internal.swift | 2 +- .../RemindersTests/RemindersListsTests.swift | 9 +++-- Package.swift | 2 +- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ce14234a..cdbb400e 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "eefcdaa88d2e2fd82e3405dfb6eb45872011a0b5", - "version" : "1.9.3" + "branch" : "async-dependencies-trait", + "revision" : "08c2d6bd189303138d729f00d97bca4a7247a2c7" } }, { diff --git a/Examples/Reminders/RemindersApp.swift b/Examples/Reminders/RemindersApp.swift index a4222ded..8c8cf588 100644 --- a/Examples/Reminders/RemindersApp.swift +++ b/Examples/Reminders/RemindersApp.swift @@ -14,8 +14,10 @@ struct RemindersApp: App { init() { if context == .live { - try! prepareDependencies { - try $0.bootstrapDatabase() + Task { + try! await prepareDependencies { + try await $0.bootstrapDatabase() + } } } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 31b0bbef..4561dcd9 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -98,7 +98,7 @@ struct ReminderText: FTS5 { } extension DependencyValues { - mutating func bootstrapDatabase() throws { + mutating func bootstrapDatabase() async throws { defaultDatabase = try Reminders.appDatabase() defaultSyncEngine = try SyncEngine( for: defaultDatabase, @@ -106,10 +106,12 @@ extension DependencyValues { RemindersListAsset.self, Reminder.self, Tag.self, - ReminderTag.self + ReminderTag.self, + startImmediately: false ) + try await defaultSyncEngine.start() if context != .live { - try defaultDatabase.write { db in + try await defaultDatabase.write { db in try db.seedSampleData() } } @@ -118,7 +120,6 @@ extension DependencyValues { func appDatabase() throws -> any DatabaseWriter { @Dependency(\.context) var context - let database: any DatabaseWriter var configuration = Configuration() configuration.foreignKeysEnabled = true configuration.prepareDatabase { db in @@ -133,21 +134,17 @@ func appDatabase() throws -> any DatabaseWriter { } #endif } - if context == .preview { - database = try DatabaseQueue(configuration: configuration) - } else { - let path = - context == .live - ? URL.documentsDirectory.appending(component: "db.sqlite").path() - : URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() - logger.debug( - """ - App database: - open "\(path)" - """ - ) - database = try DatabasePool(path: path, configuration: configuration) - } + let path = + context == .live + ? URL.documentsDirectory.appending(component: "db.sqlite").path() + : URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() + let database = try DatabasePool(path: path, configuration: configuration) + logger.debug( + """ + App database: + open "\(path)" + """ + ) var migrator = DatabaseMigrator() #if DEBUG migrator.eraseDatabaseOnSchemaChange = true diff --git a/Examples/RemindersTests/Internal.swift b/Examples/RemindersTests/Internal.swift index b21916c3..cde29aa2 100644 --- a/Examples/RemindersTests/Internal.swift +++ b/Examples/RemindersTests/Internal.swift @@ -10,7 +10,7 @@ import Testing .dependency(\.continuousClock, ImmediateClock()), .dependency(\.date.now, Date(timeIntervalSince1970: 1_234_567_890)), .dependency(\.uuid, .incrementing), - .dependencies { try $0.bootstrapDatabase() }, + .dependencies { try await $0.bootstrapDatabase() }, .snapshots(record: .failed) ) struct BaseTestSuite {} diff --git a/Examples/RemindersTests/RemindersListsTests.swift b/Examples/RemindersTests/RemindersListsTests.swift index 45c3560d..d0fbb73b 100644 --- a/Examples/RemindersTests/RemindersListsTests.swift +++ b/Examples/RemindersTests/RemindersListsTests.swift @@ -24,7 +24,8 @@ extension BaseTestSuite { color: 1218047999, position: 1, title: "Personal" - ) + ), + share: nil ), [1]: RemindersListsModel.ReminderListState( remindersCount: 2, @@ -33,7 +34,8 @@ extension BaseTestSuite { color: 3985191935, position: 2, title: "Family" - ) + ), + share: nil ), [2]: RemindersListsModel.ReminderListState( remindersCount: 2, @@ -42,7 +44,8 @@ extension BaseTestSuite { color: 2992493567, position: 3, title: "Business" - ) + ), + share: nil ) ] """ diff --git a/Package.swift b/Package.swift index 9c740c67..f27c6b85 100644 --- a/Package.swift +++ b/Package.swift @@ -31,7 +31,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), .package(url: "https://github.com/groue/GRDB.swift", from: "7.6.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", branch: "async-dependencies-trait"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), .package( From a602f45238ee93dd9737790ad393f8cc17d84306 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Sep 2025 16:29:48 -0700 Subject: [PATCH 4/5] wip --- Examples/Reminders/Schema.swift | 15 ++++++--------- Examples/RemindersTests/Internal.swift | 2 +- Package.swift | 2 +- Sources/SQLiteData/CloudKit/SyncEngine.swift | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 4561dcd9..e4308468 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -98,7 +98,7 @@ struct ReminderText: FTS5 { } extension DependencyValues { - mutating func bootstrapDatabase() async throws { + mutating func bootstrapDatabase() throws { defaultDatabase = try Reminders.appDatabase() defaultSyncEngine = try SyncEngine( for: defaultDatabase, @@ -106,15 +106,8 @@ extension DependencyValues { RemindersListAsset.self, Reminder.self, Tag.self, - ReminderTag.self, - startImmediately: false + ReminderTag.self ) - try await defaultSyncEngine.start() - if context != .live { - try await defaultDatabase.write { db in - try db.seedSampleData() - } - } } } @@ -323,6 +316,10 @@ func appDatabase() throws -> any DatabaseWriter { } ) .execute(db) + + if context != .live { + try db.seedSampleData() + } } return database diff --git a/Examples/RemindersTests/Internal.swift b/Examples/RemindersTests/Internal.swift index cde29aa2..b21916c3 100644 --- a/Examples/RemindersTests/Internal.swift +++ b/Examples/RemindersTests/Internal.swift @@ -10,7 +10,7 @@ import Testing .dependency(\.continuousClock, ImmediateClock()), .dependency(\.date.now, Date(timeIntervalSince1970: 1_234_567_890)), .dependency(\.uuid, .incrementing), - .dependencies { try await $0.bootstrapDatabase() }, + .dependencies { try $0.bootstrapDatabase() }, .snapshots(record: .failed) ) struct BaseTestSuite {} diff --git a/Package.swift b/Package.swift index f27c6b85..9c740c67 100644 --- a/Package.swift +++ b/Package.swift @@ -31,7 +31,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), .package(url: "https://github.com/groue/GRDB.swift", from: "7.6.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", branch: "async-dependencies-trait"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), .package( diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 6ad08ddb..90e0ba31 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -74,7 +74,7 @@ privateTables: repeat (each T2).Type, containerIdentifier: String? = nil, defaultZone: CKRecordZone = CKRecordZone(zoneName: "co.pointfree.SQLiteData.defaultZone"), - startImmediately: Bool = true, + startImmediately: Bool = !isTesting, logger: Logger = isTesting ? Logger(.disabled) : Logger(subsystem: "SQLiteData", category: "CloudKit") ) throws From 3ef7fd4f91028b9136ca79a102c57d510b10a42a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Sep 2025 16:31:47 -0700 Subject: [PATCH 5/5] wip --- Examples/Reminders/RemindersApp.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Examples/Reminders/RemindersApp.swift b/Examples/Reminders/RemindersApp.swift index 8c8cf588..a4222ded 100644 --- a/Examples/Reminders/RemindersApp.swift +++ b/Examples/Reminders/RemindersApp.swift @@ -14,10 +14,8 @@ struct RemindersApp: App { init() { if context == .live { - Task { - try! await prepareDependencies { - try await $0.bootstrapDatabase() - } + try! prepareDependencies { + try $0.bootstrapDatabase() } } }