Guard every persisted read of retained workout models with isLive

A remotely deleted @Model traps on any persisted-property read, including
the observed expression of .onChange, which SwiftUI evaluates on every
body pass. Guard those expressions, the onAppear takeover read, and the
summary sheet's document mapping.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
This commit is contained in:
2026-07-12 00:37:05 -04:00
parent 8854ad59d5
commit 7b6c39d8f0
4 changed files with 38 additions and 21 deletions
+12 -6
View File
@@ -71,24 +71,30 @@ struct ExerciseView: View {
}
.onAppear {
refreshDocIfNeeded()
// Take over this run: the watch parks and locks it while we're editing here.
services.watchBridge.setEditingWorkout(workout.id)
// Take over this run: the watch parks and locks it while we're editing
// here. Guarded even `id` is a persisted property and traps when read
// on an entity a remote delete has already reaped.
if workout.isLive {
services.watchBridge.setEditingWorkout(workout.id)
}
}
.onDisappear {
services.watchBridge.setEditingWorkout(nil)
}
// Reflect external changes (e.g. a set completed on the watch) live. Each edit
// rewrites the whole workout file, so the cache always holds the latest pulling
// it in can't revert a newer local tap.
.onChange(of: workout.updatedAt) { _, _ in
// it in can't revert a newer local tap. The observed expression is evaluated on
// every body pass, so it must be liveness-guarded like any persisted read.
.onChange(of: workout.isLive ? workout.updatedAt : nil) { _, _ in
absorbExternalUpdate()
}
}
/// Pull an externally-changed workout into the working copy so the plan and notes
/// update without leaving and re-entering the screen.
/// update without leaving and re-entering the screen. Skipped if the entity died
/// (remote delete) mapping a dead @Model traps.
private func absorbExternalUpdate() {
doc = WorkoutDocument(from: workout)
if let fresh = WorkoutDocument(fromLive: workout) { doc = fresh }
}
@ViewBuilder