Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
e9de34355b
commit
b67de2454a
@ -85,7 +85,9 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
runDataIntegrityMigrationIfNeeded()
|
||||
loadRitualsIfNeeded()
|
||||
if !isRunningTests {
|
||||
observeRemoteChanges()
|
||||
startObservingCloudKitRemoteChanges {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,27 +95,6 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
nowProvider()
|
||||
}
|
||||
|
||||
/// Observes CloudKit remote change notifications to auto-refresh UI when iCloud data syncs.
|
||||
private func observeRemoteChanges() {
|
||||
cloudKitSyncManager.startObserving { [weak self] in
|
||||
self?.handleRemoteStoreChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRemoteStoreChange() {
|
||||
let result = cloudKitSyncManager.processObservedRemoteChange(
|
||||
modelContext: &modelContext,
|
||||
modelContainer: modelContainer
|
||||
)
|
||||
Design.debugLog(
|
||||
"RitualStore.CloudKitSync: received remote store change #\(result.eventCount); " +
|
||||
"rebuiltContext=\(result.didRebuildModelContext); reloading rituals"
|
||||
)
|
||||
reloadData()
|
||||
// Also refresh widgets when data arrives from other devices
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
var activeRitual: Ritual? {
|
||||
currentRituals.first
|
||||
}
|
||||
@ -198,7 +179,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
SoundManager.shared.playSystemSound(SystemSound.success)
|
||||
}
|
||||
}
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
func ritualDayIndex(for ritual: Ritual) -> Int {
|
||||
@ -322,7 +303,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
var arcs = ritual.arcs ?? []
|
||||
arcs.append(newArc)
|
||||
ritual.arcs = arcs
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Starts a new arc for a past ritual (one without an active arc).
|
||||
@ -340,7 +321,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
|
||||
if let currentArc = ritual.currentArc {
|
||||
currentArc.isActive = false
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -709,7 +690,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
durationDays: defaultDuration,
|
||||
habits: habits
|
||||
)
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
private func createRitualWithInitialArc(
|
||||
@ -783,7 +764,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
durationDays: durationDays,
|
||||
habits: habits
|
||||
)
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Creates a ritual from a preset template
|
||||
@ -823,7 +804,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
durationDays: preset.durationDays,
|
||||
habits: habits
|
||||
)
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
return ritual
|
||||
}
|
||||
|
||||
@ -852,13 +833,13 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
currentArc.endDate = newEndDate
|
||||
}
|
||||
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Permanently deletes a ritual and all its history
|
||||
func deleteRitual(_ ritual: Ritual) {
|
||||
modelContext.delete(ritual)
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Adds a habit to the current arc of a ritual
|
||||
@ -870,7 +851,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
var updatedHabits = habits
|
||||
updatedHabits.append(habit)
|
||||
arc.habits = updatedHabits
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Removes a habit from the current arc of a ritual
|
||||
@ -880,7 +861,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
habits.removeAll { $0.id == habit.id }
|
||||
arc.habits = habits
|
||||
modelContext.delete(habit)
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
private func loadRitualsIfNeeded() {
|
||||
@ -1003,20 +984,18 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
}
|
||||
}
|
||||
|
||||
private func saveContext() {
|
||||
do {
|
||||
try modelContext.save()
|
||||
reloadData()
|
||||
// Widget timeline reloads can destabilize test hosts; skip in tests.
|
||||
if !isRunningTests {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
// Trigger a UI refresh for observation-based views
|
||||
analyticsNeedsRefresh = true
|
||||
insightCardsNeedRefresh = true
|
||||
} catch {
|
||||
lastErrorMessage = error.localizedDescription
|
||||
func didSaveAndReloadData() {
|
||||
// Widget timeline reloads can destabilize test hosts; skip in tests.
|
||||
if !isRunningTests {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
// Trigger a UI refresh for observation-based views
|
||||
analyticsNeedsRefresh = true
|
||||
insightCardsNeedRefresh = true
|
||||
}
|
||||
|
||||
func handleSaveAndReloadError(_ error: Error) {
|
||||
lastErrorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
private func updateDerivedData() {
|
||||
@ -1539,7 +1518,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate
|
||||
}
|
||||
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Clears all completion data (for testing).
|
||||
@ -1551,7 +1530,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
/// Simulates arc completion by setting the first active arc's end date to yesterday.
|
||||
@ -1572,7 +1551,7 @@ final class RitualStore: RitualStoreProviding, SwiftDataCloudKitStore {
|
||||
let arcDuration = arc.durationDays
|
||||
arc.startDate = calendar.date(byAdding: .day, value: -arcDuration, to: yesterday) ?? yesterday
|
||||
|
||||
saveContext()
|
||||
saveAndReload()
|
||||
|
||||
// Trigger the completion check - this will set ritualNeedingRenewal
|
||||
checkForCompletedArcs()
|
||||
|
||||
@ -42,9 +42,10 @@ Entitlements should reference variables, not hard-coded values, where possible.
|
||||
- Prefer Bedrock `SwiftDataCloudKitSyncManager` as the reusable remote observer/lifecycle component.
|
||||
- Prefer Bedrock `SwiftDataStore` + `SwiftDataCloudKitStore` to avoid app-specific pulse boilerplate.
|
||||
- Observe `.NSPersistentStoreRemoteChange` to detect remote merges.
|
||||
- On remote change, call Bedrock `processObservedRemoteChange(modelContext:modelContainer:)` before refetch.
|
||||
- Use Bedrock `startObservingCloudKitRemoteChanges(...)` for default remote-change handling.
|
||||
- Rebuild long-lived contexts only when safe (`hasChanges == false`) to avoid dropping unsaved local edits.
|
||||
- Implement protocol reload hook (`reloadData`) to run your store-specific fetch step.
|
||||
- Prefer Bedrock `saveAndReload()` + protocol hooks (`didSaveAndReloadData`, `handleSaveAndReloadError`) instead of local save wrappers.
|
||||
- Keep iOS-on-Mac pulsing loop in the root scene lifecycle (`active` only, cancel on `background`).
|
||||
- Keep a foreground fallback refresh as a safety net; gate it with Bedrock `hasReceivedRemoteChange(since:)`.
|
||||
- Emit structured logs for remote sync events (event count + timestamp) for debugging.
|
||||
@ -80,8 +81,9 @@ Before shipping any new SwiftData+CloudKit app:
|
||||
- [ ] Capabilities: iCloud/CloudKit + Push + Remote Notifications are enabled
|
||||
- [ ] Entitlements include `aps-environment` and correct container IDs
|
||||
- [ ] xcconfig defines `APS_ENVIRONMENT` per configuration
|
||||
- [ ] Remote change observer uses Bedrock `processObservedRemoteChange(...)` + reloads data
|
||||
- [ ] Remote change observer uses Bedrock `startObservingCloudKitRemoteChanges(...)`
|
||||
- [ ] Store conforms to Bedrock `SwiftDataCloudKitStore`
|
||||
- [ ] Save flows use Bedrock `saveAndReload()` and protocol hooks (no local wrapper duplication)
|
||||
- [ ] Foreground fallback is gated by Bedrock `hasReceivedRemoteChange(since:)`
|
||||
- [ ] UI has deterministic invalidation on remote reload
|
||||
- [ ] Two-device batch-update test passes without manual refresh
|
||||
|
||||
@ -83,25 +83,7 @@ final class AppDataStore: SwiftDataCloudKitStore {
|
||||
self.modelContainer = modelContext.container
|
||||
self.modelContext = modelContext
|
||||
reloadEntities()
|
||||
observeRemoteChanges()
|
||||
}
|
||||
|
||||
private func observeRemoteChanges() {
|
||||
cloudKitSyncManager.startObserving { [weak self] in
|
||||
self?.handleRemoteStoreChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRemoteStoreChange() {
|
||||
let result = cloudKitSyncManager.processObservedRemoteChange(
|
||||
modelContext: &modelContext,
|
||||
modelContainer: modelContainer
|
||||
)
|
||||
Design.debugLog(
|
||||
"Received remote store change #\(result.eventCount); " +
|
||||
"rebuiltContext=\(result.didRebuildModelContext); reloading"
|
||||
)
|
||||
reloadEntities()
|
||||
startObservingCloudKitRemoteChanges()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
@ -112,6 +94,19 @@ final class AppDataStore: SwiftDataCloudKitStore {
|
||||
reloadEntities()
|
||||
}
|
||||
|
||||
func updateEntity(_ entity: PrimaryEntity) {
|
||||
// mutate entity fields...
|
||||
saveAndReload()
|
||||
}
|
||||
|
||||
func didSaveAndReloadData() {
|
||||
// optional post-save side effects
|
||||
}
|
||||
|
||||
func handleSaveAndReloadError(_ error: Error) {
|
||||
Design.debugLog("AppDataStore: save failed: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
private func reloadEntities() {
|
||||
do {
|
||||
entities = try modelContext.fetch(FetchDescriptor<PrimaryEntity>())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user