Fix: stop horizontal bounce/swipe on Liquid Today (iOS) - #1532
Conversation
Update: follow-up fix addedThe first commit here ( Root cause: Second commit here fixes this properly: reads the real viewport width via a Verified with |
Update: widened to an app-wide fixA recurrence was reported on the Coach screen too. Root cause: Coach (like 40 other screens) renders through the shared This commit:
One merge conflict during cherry-pick, in Verified: |
|
Thanks for this — the bug is real, the diagnosis of why Today was the odd screen out is correct, and building the app locally because My concern is scope, not correctness of the idea. The diff does considerably more than the description says, and I think the extra part needs to be separated out. What the description says vs what the diff doesThe summary describes one change: "This adds the identical The diff is 23 files: 31 That is a much bigger change than the title or summary prepares a reviewer for, and it's the part I can't sign off on as written. Three specific problems with the width cap1. It is not iOS-only. The 2. "Doesn't touch layout" is contradicted by the PR's own comments. The summary argues the column never overflows ("it doesn't here"). The code comment argues the opposite, and that is the entire justification for the cap:
Both cannot be true. If nothing overflows, the cap is unnecessary; if something does, the cap is a layout change. 3. The failure mode it chooses is clipping. Also from the comment: "an oversized child is clipped in place instead of ever being able to widen the reported content size". So in the scenario the comment itself names — larger Dynamic Type — content that could previously be panned into view is now clipped and unreachable. Trading a cosmetic horizontal bounce for potentially unreachable content at large text sizes is a bad trade, and it is the accessibility case most likely to be hit by the users who need it most. Your test plan covers swiping around Today; it does not mention Dynamic Type, which is where this would bite. What I'd suggestSplit it. The change your title and summary describe — The Nothing here needs hardware, and no analytics, storage or protocol logic is touched — so once the scope is settled this should be quick. |
Adds `.scrollBounceBehavior(.basedOnSize, axes: .horizontal)` to LiquidTodayView's ScrollView. ScreenScaffold already prevents spurious left-right drift/bounce on vertical scrolls for every other tab; Today runs its own ScrollView and never got the fix. `.basedOnSize` only permits horizontal bounce when content genuinely overflows the width (it does not here, the column is width-capped), bringing Today's scroll behaviour in line with the rest of the app
…ld screens (iOS) ryanbr#697 added `.scrollBounceBehavior(.basedOnSize, axes: .horizontal)` to ScreenScaffold, which every tab renders through. The previous commit brought LiquidTodayView (which runs its own ScrollView) in line. This commit closes the audit: every other top-level vertical ScrollView that does NOT route through ScreenScaffold — onboarding, the terms gate, the device wizards, live workout, workout selection, and various sheets/diagnostic readers — still lacked the ryanbr#697 baseline and could rubber-band left-right on a purely vertical scroll, the same quirk the rest of the app was fixed for. Each addition is the identical `#if os(iOS)`-guarded `.scrollBounceBehavior(.basedOnSize, axes: .horizontal)`, applied to the same vertical ScrollView, in the same place ScreenScaffold applies it. `.basedOnSize` only permits horizontal bounce when content genuinely overflows the width, so this does not touch layout, vertical scrolling, pull-to-refresh, or any intentionally-horizontal inner ScrollView (pace/ duration pills, workout chips, calendar heat strip, journal day pills, code blocks). DEBUG-only #Preview/demo-host scroll views are left alone. This is the same modifier, not a width cap: ScreenScaffold itself is untouched here (it already carries ryanbr#697), and no GeometryReader or hard .frame(width:) is introduced anywhere. macOS is unchanged. Verified: xcodegen generate + xcodebuild for both NOOPiOS (iPhone 17 simulator) and Strand (macOS) — BUILD SUCCEEDED on both, zero errors.
bbb1437 to
4814b4f
Compare
|
Thanks for the detailed review — the diagnosis was right and the scope concern was fair. I've reworked the PR so the diff now matches what the title and summary describe. What changed
Verification
The width-cap mechanism is left for a separate PR with the iOS-only |
|
Re-reviewed the rework. This is exactly what I was asking for — all three concerns are resolved by removal rather than patched around, and the diff now matches its title. Verified, not taken on trust
One thing I flagged to myself and then cleared
On the breadth22 files still reads wide for a bug reported on one screen, but the principle is now legible from the diff itself: every screen that builds its own Good response to review — removing the mechanism rather than defending it was the right call, and the two-commit split makes the reasoning easy to follow. Ready to merge from my side. |
…S) (#1549) #1532 established the rule -- every screen that builds its OWN ScrollView gets the horizontal-bounce suppression ScreenScaffold has carried since #697 -- and applied it to 28 scroll views. Four production sites were left behind: CoachView the chat transcript BreathingView the technique/detail column MetricExplorerView the metric list ManualWorkoutSheet its second ScrollView (the first got the modifier) Coach is the one that matters: it is the screen a recurrence was actually reported on, and it renders through ScreenScaffold AND nests its own ScrollView, so the scaffold's modifier never reached the scroller the user was touching. That is the case #1532 was widened to address and did not quite close. Deliberately NOT touched: six further sites a scan flags, all inside #Preview or #if DEBUG blocks (ChargeBreakdownFormat x2, WeeklyDigestView, TrendsReportView, SkinTempCardsView, StressView). Preview code does not ship, and a behaviour modifier there would be noise that reads as coverage. The sweep is now complete and the arithmetic closes: the app has 39 vertical ScrollViews and 8 horizontal. Of the 39 -- ScreenScaffold's own from #697, 28 from #1532, 4 here, and the remaining 6 are the preview sites. Zero production gaps. `ScrollView {`, `ScrollView(.vertical, showsIndicators:)` and `ScrollView(.horizontal, ...)` are the only invocation forms in the tree, so nothing is hiding behind a shape the scan did not know about. Same modifier and same `#if os(iOS)` guard as #1532 -- `.scrollBounceBehavior` is macOS 13.3+ against this project's macOS 13.0 target, so the guard is load-bearing rather than stylistic. `.basedOnSize` permits horizontal bounce only when content genuinely overflows the width, so nothing meant to scroll sideways is affected. No horizontal ScrollView was touched: each insertion was brace-matched back to its scroller and confirmed vertical at depth 0. No Android twin. This is a SwiftUI quirk -- a purely vertical ScrollView rubber-banding left-right. Compose scroll axes are explicit, so a verticalScroll column cannot drift horizontally without an explicit horizontalScroll, and TodayScreen has none. A twin here would be code with no bug behind it. Verified: app-build green on both legs, Test Strand 1195 tests 0 failures -- app-build.yml is disabled by default, so nothing else compiles these files. Doc lint clean. NOT hardware-tested: whether each of these four can actually be made to drift needs a device, the same limit #1532 hit. The change is the one-line modifier already shipped on 28 other scrollers.
Summary
On iOS, every screen renders through
ScreenScaffold, which already carries the fix from #697 for a SwiftUI quirk where a purely-verticalScrollViewcan still rubber-band/drift left-right (.scrollBounceBehavior(.basedOnSize, axes: .horizontal)).LiquidTodayView(the default Today/Home tab,liquidTodayEnabled = true) builds its own separateScrollViewinstead of going throughScreenScaffold, and never received that fix — so Today was the one screen left with a spurious horizontal swipe/scroll while every other tab felt vertical-only. This adds the identical#if os(iOS)-guarded modifier toLiquidTodayView'sScrollView, in the same placeScreenScaffoldapplies it, bringing Today in line with the rest of the app.ScrollViewthat does not route throughScreenScaffold(onboarding, the terms gate, the device wizards, live workout, workout selection, and various sheets/diagnostic readers) gets the same modifier, so no vertical screen is left with the spurious horizontal bounce.ScreenScaffolditself is untouched — it already carries Recovery (Charge) won't update. #697.This is the same modifier, not a width cap. There is no
GeometryReader, no hard.frame(width:), and no layout change anywhere in this PR —.basedOnSizeonly allows horizontal bounce when content genuinely overflows the width, so vertical scrolling, pull-to-refresh, and the intentionally-horizontal innerScrollViews (pace/duration pills, workout chips, coach suggestions, calendar heat strip, journal day pills, code blocks) are untouched. macOS is unchanged. DEBUG-only#Preview/demo-host scroll views are left alone.iOS-only change; no analytics/storage/protocol logic touched, so no Android twin needed.
Scope change vs. the original diff
Per @ryanbr's review, the
GeometryReader+ hard.frame(width: rootGeo.size.width)cap mechanism (previously inScreenScaffold.swiftandLiquidTodayView.swift) has been removed entirely. The reviewer's three concerns all applied to that mechanism: it was not iOS-only (theGeometryReadersat outside the#ifon macOS), it contradicted the "doesn't touch layout" claim, and its failure mode was clipping potentially-unreachable content at large Dynamic Type sizes. That mechanism belongs in its own PR with an iOS-onlyGeometryReaderguard and a Dynamic Type test pass; it is not in this one. What remains is only the.scrollBounceBehavior(.basedOnSize, axes: .horizontal)additions — the part the review said it would take today.Test plan
xcodegen generate && xcodebuild -scheme NOOPiOS -destination 'platform=iOS Simulator,id=…' CODE_SIGNING_ALLOWED=NO build— BUILD SUCCEEDED (iPhone 17 Pro simulator, Xcode 26.6).xcodebuild -scheme Strand -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build— BUILD SUCCEEDED (macOS reference leg, unchanged by this PR).Generated with Devin