diff --git a/Kanban/UI/Board/BoardView.swift b/Kanban/UI/Board/BoardView.swift index c1b9a72..4531074 100644 --- a/Kanban/UI/Board/BoardView.swift +++ b/Kanban/UI/Board/BoardView.swift @@ -121,7 +121,7 @@ struct BoardView: View { private let spacing: CGFloat = 12 var body: some View { - GeometryReader { _ in + GeometryReader { proxy in // The strip's slots: the lanes the strip should *show*, with the drag's N contiguous // shadows opened at the proposal. Recomputed on every render, so a foreign reload // mid-drag simply moves the zones (rule 1 of 04-interactions.md ▸ Drag and drop's @@ -129,7 +129,7 @@ struct BoardView: View { let slots = stripSlots ZStack(alignment: .topLeading) { backdrop - laneStrip(slots) + laneStrip(slots, standard: standardWidth(stripWidth: proxy.size.width)) } .padding(spacing) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -254,8 +254,7 @@ struct BoardView: View { /// The lanes and the drag's shadows, plus the trash column when it is shown. @ViewBuilder - private func laneStrip(_ slots: [StripSlot]) -> some View { - let standard = standardWidth + private func laneStrip(_ slots: [StripSlot], standard: CGFloat) -> some View { HStack(alignment: .top, spacing: spacing) { ForEach(slots) { slot in switch slot { @@ -480,11 +479,20 @@ struct BoardView: View { gap: spacing, window: window, stripFrame: { laneDrops.stripFrame }, - standard: { standardWidth } + standard: { standardWidth(stripWidth: laneDrops.stripFrame.width) } ) } - /// The strip's 1× lane width. + /// The strip's 1× lane width, for a strip `stripWidth` points wide. + /// + /// **The width is a parameter, and which width the caller passes matters.** Body evaluation + /// passes the `GeometryReader`'s live proposal, because that is the one source that invalidates + /// the view when it changes — the registry's `stripFrame` is deliberately unobserved (it exists + /// for event-time reads), so a body computed from it renders the *previous* layout's width: on + /// first open that is `.zero`, every lane collapses to `standardWidth`'s 1pt floor, and the + /// board stays that way until an unrelated invalidation happens by. The drop delegates pass + /// `laneDrops.stripFrame.width` instead, which is the same number read at event time — exactly + /// the registry's purpose. The two agree because the padded container fills the reader. /// /// During a resize session the standard is **frozen** at its drag-start value: the window is /// animating mid-resize, so deriving the standard from the live width would feed that animation @@ -504,12 +512,12 @@ struct BoardView: View { /// - a **cross-board lane arrival**'s units while its shadow hovers here, by the same rule read /// from the destination's side: the shadow occupies its units, and the strip has to make room /// for them or the shadow would be drawn at a width the lane will not have. - private var standardWidth: CGFloat { + private func standardWidth(stripWidth: CGFloat) -> CGFloat { if resize.isActive { return resize.standard } var units = LaneLayoutMath.totalUnits(of: liveLanes, trashUnits: isTrashVisible ? 1 : 0) units += arrivingLaneUnits return LaneLayoutMath.standardWidth( - stripWidth: laneDrops.stripFrame.width, + stripWidth: stripWidth, totalUnits: units, gap: spacing )