diff --git a/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md b/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md index 3ec7ab4..c551516 100644 --- a/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md +++ b/Sources/Bedrock/Storage/SWIFTDATA_CLOUDKIT_SETUP_GUIDE.md @@ -83,21 +83,10 @@ final class AppDataStore: SwiftDataCloudKitStore { init(modelContext: ModelContext) { self.modelContainer = modelContext.container self.modelContext = modelContext - startSyncObservation() + startObservingCloudKitRemoteChanges() reloadData() } - private func startSyncObservation() { - cloudKitSyncManager.startObserving { [weak self] in - guard let self else { return } - _ = self.cloudKitSyncManager.processObservedRemoteChange( - modelContext: &self.modelContext, - modelContainer: self.modelContainer - ) - self.reloadData() - } - } - func reloadData() { // Fetch your app entities and recompute derived state. refreshVersion &+= 1 @@ -105,6 +94,29 @@ final class AppDataStore: SwiftDataCloudKitStore { } ``` +If your store needs post-remote side effects (for example widget timeline reload), use: + +```swift +startObservingCloudKitRemoteChanges { + WidgetCenter.shared.reloadAllTimelines() +} +``` + +If your store needs save side effects/error handling, prefer Bedrock protocol hooks: + +```swift +func didSaveAndReloadData() { + // optional side effects (widgets, cache invalidation, etc.) +} + +func handleSaveAndReloadError(_ error: Error) { + // set store error state +} + +// Then call the shared default: +saveAndReload() +``` + ## 4) Integrate `SwiftDataRefreshThrottler` Use throttling for on-appear and tab/scene refresh paths: diff --git a/Sources/Bedrock/Storage/SwiftDataStore.swift b/Sources/Bedrock/Storage/SwiftDataStore.swift index cfeb0f0..6015e64 100644 --- a/Sources/Bedrock/Storage/SwiftDataStore.swift +++ b/Sources/Bedrock/Storage/SwiftDataStore.swift @@ -2,7 +2,7 @@ // SwiftDataStore.swift // Bedrock // -// Generic macOS fallback pulse wiring for SwiftData + CloudKit stores. +// Shared store protocols/defaults for SwiftData, including CloudKit helpers. // import Foundation @@ -15,6 +15,47 @@ public protocol SwiftDataStore: AnyObject { /// App-specific fetch/recompute entry point. func reloadData() + + /// App-specific side effects after a successful save+reload cycle. + func didSaveAndReloadData() + + /// App-specific error handling when save+reload fails. + func handleSaveAndReloadError(_ error: Error) +} + +public extension SwiftDataStore { + /// Default no-op success hook. + func didSaveAndReloadData() {} + + /// Default error handling hook. + func handleSaveAndReloadError(_ error: Error) { + Design.debugLog( + "\(String(describing: type(of: self))): failed saveAndReload error=\(error.localizedDescription)" + ) + } + + /// Saves the context, reloads data, then runs optional hooks. + /// + /// If hooks are omitted, protocol defaults are used: + /// - success: `didSaveAndReloadData()` + /// - failure: `handleSaveAndReloadError(_:)` + func saveAndReload( + onSuccess: (() -> Void)? = nil, + onFailure: ((Error) -> Void)? = nil + ) { + do { + try modelContext.save() + reloadData() + (onSuccess ?? didSaveAndReloadData)() + } catch { + (onFailure ?? handleSaveAndReloadError)(error) + } + } + + /// Convenience overload that always uses protocol hooks. + func saveAndReload() { + saveAndReload(onSuccess: didSaveAndReloadData, onFailure: handleSaveAndReloadError) + } } @MainActor @@ -38,6 +79,33 @@ public extension SwiftDataCloudKitStore { cloudKitSyncManager.hasReceivedRemoteChange(since: date) } + /// Starts observing remote CloudKit-backed store changes. + /// + /// - Parameter afterReload: Optional callback that runs after the default `reloadData()` handling. + func startObservingCloudKitRemoteChanges(afterReload: (@MainActor () -> Void)? = nil) { + cloudKitSyncManager.startObserving { [weak self] in + self?.handleObservedCloudKitRemoteChange(afterReload: afterReload) + } + } + + /// Applies the default handling for an observed remote CloudKit merge. + /// + /// - Parameter afterReload: Optional callback that runs after `reloadData()`. + func handleObservedCloudKitRemoteChange(afterReload: (@MainActor () -> Void)? = nil) { + let result = cloudKitSyncManager.processObservedRemoteChange( + modelContext: &modelContext, + modelContainer: modelContainer + ) + + Design.debugLog( + "\(String(describing: type(of: self))): received remote store change #\(result.eventCount); " + + "rebuiltContext=\(result.didRebuildModelContext)" + ) + + reloadData() + afterReload?() + } + /// Production fallback: pulses CloudKit import scheduling while app is active on Mac runtime. func forceCloudKitImportPulse(reason: String) { guard cloudKitSyncManager.triggerMacImportPulse(