# Bug Analysis: Timeline Instance Delete Dismisses Habit Tray ## Root Cause: Presentation Layer Mismatch The iOS UIKit rule at the heart of this: **a view controller cannot present a new modal if it already has a presented view controller.** If it tries, the existing presented VC is typically dismissed first. ### The presentation stack ``` ProjectDawnApp └── DayView ← the presenting VC └── .sheet(.constant(true)) → HabitTrayView ← the presented VC (tray) └── .sheet($showAddHabit) → HabitFormView ``` ### Why it breaks `InstancePillView` lives in `DayTimelineView`, which is part of `DayView`'s content tree — underneath the tray sheet in the UIKit hierarchy. When `.contextMenu` or `.confirmationDialog` fires from `InstancePillView`, UIKit must present the resulting `UIAlertController` (or context menu overlay) **from `DayView`'s VC**. But `DayView`'s VC is already presenting `HabitTrayView`. UIKit sees a VC trying to present something new while it already has a presented VC, and resolves the conflict by dismissing the tray — which is exactly the symptom. The `.contextMenu` modifier makes it worse: on iOS, long-pressing content beneath a sheet can cause UIKit to actively dismiss the covering sheet in order to surface the context menu's "peek" of the underlying content. That explains why the tray vanishes *before* the confirmation even appears. The bug report's conclusion is correct: **the delete confirmation is being launched from the wrong presentation layer.** The confirmation needs to originate from inside the tray sheet (the topmost presented VC), not from the timeline content underneath it. --- ## What Is NOT the Issue - Not a SwiftData problem — the delete itself would work fine if the UI could present correctly. - Not specific to `confirmationDialog` vs `alert` — any modal presentation from the timeline subtree hits the same wall, which is why the alternate `alert` approach also failed. - Not a race condition or timing issue. --- ## Fix Options ### Option 1 — Inline SwiftUI confirmation within the pill Replace the `.confirmationDialog` with a small "confirm delete?" overlay rendered directly inside `InstancePillView` using `ZStack`/`.overlay`. No UIKit presentation machinery is triggered, so the tray is never disturbed. This is the least invasive fix. **Tradeoff:** Non-standard UX pattern — users expect a system-level confirmation for destructive actions. Requires custom styling to communicate clearly. ### Option 2 — Present confirmation from inside `HabitTrayView` `HabitTrayView` sits at the top of the presentation stack, so it can safely present. Communicate the "instance to delete" from `InstancePillView` up to `HabitTrayView` via a shared environment value or coordinator, then let `HabitTrayView` own and present the confirmation dialog. **Tradeoff:** More wiring — `InstancePillView` needs a way to signal delete intent upward through the view tree without owning the presentation itself. ### Option 3 — Bubble delete intent to `DayView`, present from the tray sheet Store a `pendingDeleteInstance: HabitInstance?` in a shared coordinator (e.g., `HabitDragCoordinator` or a new `TimelineActionCoordinator`). `InstancePillView` sets it; `HabitTrayView` observes it and presents the confirmation from inside the sheet. This is structurally similar to Option 2 but uses the existing coordinator pattern from Phase 5 rather than a new binding chain. **Tradeoff:** Adds responsibility to the coordinator that is conceptually unrelated to drag & drop. --- ## Recommended Fix **Option 1 (inline confirmation)** is the path of least resistance for Phase 6. The delete action is low-frequency and the pill already has an expanded state (`isExpanded`) — a secondary "confirm?" state within the pill is consistent with that existing pattern and avoids touching the presentation hierarchy at all. If a system-level confirmation is strongly preferred, **Option 2** is cleaner than Option 3 because it keeps delete logic close to where instances are displayed.