Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-16 16:30:12 -06:00
parent bd09c14a02
commit db06b43364
2 changed files with 93 additions and 13 deletions

View File

@ -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:

View File

@ -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(