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..cdbb400e 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", @@ -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/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/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 31b0bbef..e4308468 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -108,17 +108,11 @@ extension DependencyValues { Tag.self, ReminderTag.self ) - if context != .live { - try defaultDatabase.write { db in - try db.seedSampleData() - } - } } } 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 +127,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 @@ -326,6 +316,10 @@ func appDatabase() throws -> any DatabaseWriter { } ) .execute(db) + + if context != .live { + try db.seedSampleData() + } } return database 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" } } 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/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index f420334b..90e0ba31 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. @@ -72,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 @@ -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