Guard NavigationLink destination inits against deleted @Models

A closure-based NavigationLink builds its destination view eagerly for
every row on every parent-list body evaluation — including the view-graph
update SwiftUI runs the instant SyncEngine.delete does context.delete +
save. Mapping the row's just-deleted @Model to a document there reads a
persisted property on a dead model, which traps in SwiftData
(_InitialBackingData.getValue -> assertionFailure). This is the "Crashed
when deleted a workout" TestFlight report on 2.3 (124).

Add WorkoutDocument(fromLive:) (nil when isDeleted) + a .deletedPlaceholder
and route every destination-init / updatedAt-absorb map through it:
phone WorkoutLogListView + SplitDetailView (read split.id) and watch
WorkoutLogListView. Fixes the whole crash class, not just the report.

Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
This commit is contained in:
2026-07-10 10:47:10 -04:00
parent 8b20d7ad08
commit 85e15823a3
5 changed files with 49 additions and 6 deletions
+24
View File
@@ -54,6 +54,30 @@ extension WorkoutDocument {
deletedLogIDs: workout.deletedLogIDs,
restSeconds: workout.restSeconds, autoAdvance: workout.autoAdvance)
}
/// Maps a *live* cache entity to a document, or `nil` when that entity has already
/// been deleted from its SwiftData context. Reading a persisted property on a
/// deleted `@Model` traps (`_InitialBackingData.getValue` `assertionFailure`), and
/// a closure-based `NavigationLink` builds its destination view and so runs this
/// map eagerly for *every* row in the parent list, including during the view-graph
/// update that fires the instant a workout is deleted. Any map of an entity not
/// already known to be live must go through this guard.
init?(fromLive workout: Workout) {
guard !workout.isDeleted else { return nil }
self.init(from: workout)
}
/// An inert stand-in for a destination whose backing entity is gone: the row is
/// being removed, so its detail screen is never actually shown. Reads nothing from
/// any entity, so it is safe to substitute for a deleted `@Model`.
static let deletedPlaceholder = WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
id: "",
start: Date(timeIntervalSinceReferenceDate: 0),
status: WorkoutStatus.notStarted.rawValue,
createdAt: Date(timeIntervalSinceReferenceDate: 0),
updatedAt: Date(timeIntervalSinceReferenceDate: 0),
logs: [])
}
// MARK: - Document Cache (upsert)