Refine the iOS visual system and key screen layouts - #1068
Conversation
ryanbr
left a comment
There was a problem hiding this comment.
Really like the direction here — the redesign looks great, the ledger doc is a nice touch, and I checked the navigation carefully: all four tabs, their destinations, and the re-tap refresh/pop-to-root behavior are all faithfully preserved, and the quick-action "+" still reaches its sheet from the header. Nicely done.
A couple of things before merge though.
One hard blocker — i18n CI will fail. The new Trends week header adds two un-localized strings:
TrendsView.swift:378—"… · \(n)/7 days"TrendsView.swift:383— the matching.accessibilityLabel
Your own ledger says the xcstrings catalogs stay excluded "unless a later UI change genuinely introduces new localized copy" — which this does, so they just need de/es/fr entries in Strand/Resources/Localizable.xcstrings (or wrap in String(localized:) and add them). python3 Tools/i18n_audit.py --ci origin/main should exit 0 after that.
"Removes redundant, shown-twice" isn't quite right for four items — these look like real removals rather than same-screen dedup. Each may be a fine call, just want them to be deliberate:
- The Today sync chip is gone from both chromes (classic + liquid) and relocated to a new Devices card. That was the only ambient sync signal on Today (the whole point of #245) — fine to move, but it's a tab away now.
- The Live-HR subtitle ("Live · beat by beat" / "5-minute average · since midnight" / "Waiting for the strap") is deleted — that live-vs-banked-average state isn't shown anywhere else.
WeeklyDigestView:594now gates the scale caption to.chargeonly, so embedded Effort gauges lose their "of 21" denominator.- The "Full day ›" affordance is removed from the live-HR card (tap-through still works, just less discoverable).
The section-header trailings you dropped elsewhere (Sleep "Rest"/"vs typical", the Trends rangeSubtitle overline) genuinely are duplicates — those are good.
Last thing: I can't compile iOS here, so this needs an app-build pass before merge, especially DevicesView (it sits right at the type-checker's budget on iOS — the new card is its own struct so probably fine, but that file has bitten us before).
Happy to take you up on splitting the pure-styling layer from the layout/content changes if that's easier — would make this quicker to land. Thanks for the work on this!
I hear you, I will fixed blocker, but some of things you said are deliberate, I fixed number 3, for number 4 I made a red arrow so that's at least will keep it clean while showing that a function does exist, fixed number 2 and I think keeping number 1 is the smartest decision. 4 buttons on the top is already a lot, 5 is too much but let me know what you think, I will push in the next few minutes UPDATE: Pushed it, made a few layout changes on the workout screen I would like you to check out that I also think look much nicer now let me know of any issues or things you wanna change and I will get on it! |
Hey, in the trends tab, the charge, effort and rest card (not the one with the gauges) is it meant to be there? It also seems kinda like a duplicate because it doesn't have any functionality over the card above it with the gauges? |
|
Will this be iOS only or will we get this for Android as well? Saw the reddit Screencast and love it 👏. |
ryanbr
left a comment
There was a problem hiding this comment.
Latest two commits look good. Light-mode adaptation is done the right way — hardcoded colors moved to adaptive Color(light:dark:) tokens, and the onDark/overSky removals are justified now that the sky and card surfaces are light-adaptive (no dark-mode break either direction). The second LiveWorkout rework is layout-only: timer, BPM, effort (liveStrain → UnitFormatter.effortValue) and zones all read from the same sources. i18n gate passes.
Also — credit where due: I earlier called the sync status "gone from Today," but the pull-refresh vessel now holds on Today while live.backfilling is true, so an in-progress offload is visible ambiently. That effectively covers the concern; only the idle "synced Xm" state lives on Devices, which is fine.
One nit (not a blocker): the effort gauge dropped its of 21 / of 100 scale-max label. For the default 0–100 scale that's fine, but on the WHOOP 0–21 scale a bare "12.3" is ambiguous without the "of 21", and there's no accessibility fallback here (the a11y label is just the number). The MODERATE/HIGH state word + gauge fill partly cover it. Could you either mirror what the weekly-digest gauge does (tap popover / a11y carries the scale) or drop the scale into the accessibility label at least? Small.
The one real gate before merge: this is 46 files of app-target Swift that can't compile on Linux, and it leans on DevicesView (which has hit the iOS type-checker budget before) plus the new Light-mode paths. It needs a clean iOS build before merge — happy to run one on my side if that helps.
Really nice work overall.
Will fix the gauges, and I'm working on some more changes to the workout screen but I have a question. Would you consider moving the workout screen to the main tabs instead of trends? I feel like workouts might be much more important and main than the trends screen |
I addressed the remaining accessibility feedback for the effort gauge. I also ran a clean iOS build after the latest changes and verified it compiles successfully. I installed the app on my iPhone as well and confirmed it launches and runs correctly. Let me know if there's anything else you'd like me to adjust before merging. |
Restructure the in-exercise screen to the glanceable hierarchy from the iOS live-workout redesign (#1068): a sport-name header with a recording-status capsule, then centered TIME / HEART RATE / EFFORT heroes, with the HR-zone label as a capsule on the zone-rail header row. Same data and sources throughout - the elapsed clock, smoothed bpm + zone tint, ActiveWorkout .liveStrain effort, avg/peak/effort stats, the sensor readout, and the #845 scroll + End-confirm are all unchanged. Effort keeps its "of 21" / "of 100" scale caption (localized de/es/fr/pt-PT/ zh) - a bare number on the WHOOP 0-21 scale is ambiguous without it, so unlike the iOS change we keep it. Display-only: no analytics, decoder, stored-data, or BLE change. Android-only (iOS has this via #1068). compileFullDebugKotlin + i18n audit green; visual layout not device-verified (Compose does not render on this Linux box).
Co-lay the Start-workout and Add-workout controls as an equal-width action row on the Workouts screen, matching the iOS workoutActionRow (#1068). When a strap is bonded they sit side by side; unbonded shows Add alone (moved up out of the range bar so it stays reachable without a strap), which also frees the range pill from the old Add-vs-5-segment width fight (#234/#339). The active-workout banner is unchanged. WorkoutStartSection now provides Add in every idle state, so the empty-state card no longer renders its own Add (that would double it) and the dead `loaded` gate is dropped. Add is paused only during an in-progress live session (the banner owns that state) and returns the moment the workout ends. Display-only: Start is still bonded-gated (a live session needs the strap), no change to when it's available. Android-only. compileFullDebugKotlin + i18n audit green; layout not device-verified (Compose does not render on Linux).
Restructure the in-exercise screen to the glanceable hierarchy from the iOS live-workout redesign (#1068): a sport-name header with a recording-status capsule, then centered TIME / HEART RATE / EFFORT heroes, with the HR-zone label as a capsule on the zone-rail header row. Same data and sources throughout - the elapsed clock, smoothed bpm + zone tint, ActiveWorkout .liveStrain effort, avg/peak/effort stats, the sensor readout, and the #845 scroll + End-confirm are all unchanged. Effort keeps its "of 21" / "of 100" scale caption (localized de/es/fr/pt-PT/ zh) - a bare number on the WHOOP 0-21 scale is ambiguous without it, so unlike the iOS change we keep it. Display-only: no analytics, decoder, stored-data, or BLE change. Android-only (iOS has this via #1068). compileFullDebugKotlin + i18n audit green; visual layout not device-verified (Compose does not render on this Linux box).
Co-lay the Start-workout and Add-workout controls as an equal-width action row on the Workouts screen, matching the iOS workoutActionRow (#1068). When a strap is bonded they sit side by side; unbonded shows Add alone (moved up out of the range bar so it stays reachable without a strap), which also frees the range pill from the old Add-vs-5-segment width fight (#234/#339). Add stays visible in every state: beside Start when bonded-idle, alone when unbonded, and below the live banner during an active workout — so no control disappears in any state. WorkoutStartSection is now the single home for Add, so the empty-state card no longer renders its own (that would double it) and the dead `loaded` gate is dropped. Display-only: Start is still bonded-gated (a live session needs the strap), no change to when it's available. Android-only. compileFullDebugKotlin + i18n audit green; layout not device-verified (Compose does not render on Linux).
Restructure the in-exercise screen to the glanceable hierarchy from the iOS live-workout redesign (#1068): a sport-name header with a recording-status capsule, then centered TIME / HEART RATE / EFFORT heroes, with the HR-zone label as a capsule on the zone-rail header row. Same data and sources throughout - the elapsed clock, smoothed bpm + zone tint, ActiveWorkout .liveStrain effort, avg/peak/effort stats, the sensor readout, and the #845 scroll + End-confirm are all unchanged. Effort keeps its "of 21" / "of 100" scale caption (localized de/es/fr/pt-PT/ zh) - a bare number on the WHOOP 0-21 scale is ambiguous without it, so unlike the iOS change we keep it. Display-only: no analytics, decoder, stored-data, or BLE change. Android-only (iOS has this via #1068). compileFullDebugKotlin + i18n audit green; visual layout not device-verified (Compose does not render on this Linux box).
Bring the Android Workouts screen closer to the iOS redesign (#1068): - Co-lay Start + Add as an equal-width action row (EXP-018). Bonded → side by side; unbonded → Add alone (moved up out of the range bar so it's reachable without a strap, which also retires the #234/#339 Add-vs-range-pill fit fix); during an active workout the live banner keeps Add below it. Add stays visible in every state; WorkoutStartSection is its single home so the empty-state card no longer doubles it, and the dead `loaded` gate is dropped. - Give the sport picker per-sport glyphs (shared `sportIcon` catalogue) so it reads by icon like the iOS workout selection screen and the Workouts list rows, not a bare text list. - Move the Clear-filters chip onto the search-field row with a 48dp hit target, matching iOS's search-row placement + 44pt target. Display-only: Start is still bonded-gated (a live session needs the strap), no change to when it's available. Android-only (iOS via #1068). No new strings. compileFullDebugKotlin + i18n audit green; layout not device-verified.
Drop the green chargeColor surface glow from the HRV/RHR small multiples, Week-in-Review, and Charge history cards, keeping the semantic color inside the data graphics. Matches the iOS #1068 Trends cleanup. Display-only, Android-only.
Restructure the in-exercise screen to the glanceable hierarchy from iOS #1068: sport-name header + recording capsule, centered TIME/HEART RATE/EFFORT heroes, HR-zone capsule on the zone-rail header. Effort keeps its localized of-N scale. Same liveStrain/bpm/zone sources; display-only, Android-only.
Hey Ryan, could I get an update on what's going on? |
|
Rebase would be a start |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds floating bottom controls with unique sport icons, a full-screen workout picker, shared Liquid Glass search fields, and Effort gauge VoiceOver scale context, with the UI customization ledger kept in lockstep. Co-authored-by: Cursor <cursoragent@cursor.com>
Add String Catalog entries for the new workout chrome, workout selection, and Liquid Glass search copy so the i18n CI gate passes without changing UI or behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
Rebase onto latest main is complete, clean iOS build passes, i18n audit passes, and I also installed & launched it successfully on my iPhone. Everything looks good on my end (the extra commit done right now also passed a clean iOS build and no merge conflicts) |
…terial. Extract LiquidScoreGauge for hero rings, upgrade LiquidRender.vessel shading, add the immersive Sleep night hero, and match Key Metrics bar fills to the gauge gradient. Co-authored-by: Cursor <cursoragent@cursor.com>
What else? |
|
Thanks for this — the new look is really nice and it's clearly gone down well with people. A few things to sort before it can go in: Blockers
A couple of things to confirm
Once the two blockers are sorted I'm glad to take another look. Thanks again for the effort here! |
Summary
This PR refreshes the iOS presentation layer and improves the layout of several key screens while preserving the existing application behavior and data flow.
The goal is to make the interface feel more consistent, readable, and polished without changing the underlying health, device, or scoring functionality.
Main changes
Intentional layout changes
This PR includes some deliberate layout adjustments in addition to styling:
Verification
The branch was audited against the latest upstream
main.Confirmed unchanged:
The changes are limited to presentation, layout, and user-interface behavior.
Testing
git diff --checkpasses.I’m happy to adjust the scope or split parts of this into smaller PRs if that would make review easier.