Skip to content

Fix ride status visibility, gear rail/readout craft, and hardware bugs - #43

Merged
sbroenne merged 8 commits into
mainfrom
sbroenne-click-button-visual-feedback
Aug 16, 2026
Merged

Fix ride status visibility, gear rail/readout craft, and hardware bugs#43
sbroenne merged 8 commits into
mainfrom
sbroenne-click-button-visual-feedback

Conversation

@sbroenne

Copy link
Copy Markdown
Owner

Squashes together this session's work since the last merge to main:

  • Restored explicit "Start Shifting" step with clearer status feedback.
  • Added comprehensive UI regression tests, then expanded coverage further (core coverage 87.68% → 90.80%, CI gate now requires ≥90%).
  • Fixed ride status visibility (wordless reconnect icon → spelled-out status banner) and gave the stop confirmation a real Cancel action (.confirmationDialog silently dropped Cancel on iOS 26 — replaced with .alert).
  • Reworked the ride screen around what only it can show: gear rail fills in gears already ridden through, readout caption sizing, easier/harder buttons told apart by weight (not color, to stay colour-blind-safe), one-row equipment status footer, low Click battery shown as a warning chip, Settings copy cleanup (no duplicate "Zwift Click", disambiguated cassette names, "Try Again" on retry).
  • Shipped as build 1.0 (10) to TestFlight.
  • Fixed two bugs found riding build 10 on a physical KICKR: the chain-position reminder vanishing (and the button jumping) the instant the trainer connected, and Demo Mode's shift buttons not getting the new easier/harder weight styling.

All test-first: 30/30 UI tests, 259 core tests, 90.80% coverage.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com
Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7

Stefan Brönner and others added 8 commits August 15, 2026 13:31
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
The ride status sat in the middle of the toolbar, squeezed between the
settings and fan controls on one side and Stop on the other. It lost its
text long before it lost its icon: a reconnecting trainer showed the
rider a bare orange warning triangle 21 points wide with nothing to
explain it. The status now sits beside the gear, where it has room to be
read, and the toolbar keeps only the gear menu.

Stopping used a confirmation dialog, which on iOS 26 renders as a compact
card that drops its cancel button entirely. A rider who tapped Stop by
mistake mid-ride was left with a destructive choice and no visible way
out. It is now an alert, which always shows both actions, and it says
what stopping actually does.

Both were found by reading rendered screens, and both are now guarded by
UI tests with measured assertions rather than mere existence checks.
The gear rail was a row of identical dots with one coloured in, which
can only be read by counting. It now fills in the gears already ridden
through and gives every marker kind its own height, so a position can be
seen rather than counted, and still reads in greyscale. The line under
the gear was a fixed large title competing with the number it describes;
it is now sized as a caption to it.

Easier and harder were two identical slabs distinguished by a small
symbol. Harder is drawn solid and easier tinted, so they differ in
weight as well as in glyph and word. Neutral grey was tried first and
read as disabled, so the lighter button keeps its tint.

The four equipment statuses were split three-and-one, orphaning the
riding app. They now share a row. A Click battery about to die was a
grey caption; it is the one thing on that screen a rider can still act
on, so it is drawn as a warning.

In settings, a Click that reports itself as "Zwift Click" no longer
repeats the row's own title, the gears row leads with the gears chosen
rather than a count of them, and cassettes that share a name are told
apart by their cog count where no heading does it for them. Retrying a
failed start says Try Again rather than Start Shifting, which read as
though nothing had been attempted.

The rail model, the read-out sizing and the cassette naming are pure
functions in the core package with tests, rather than numbers typed into
a view. Two more UI tests measure the status row and the battery
warning.
Build 9 was archived but never uploaded, so build 10 carries everything
since build 8: the per-connection statuses, the ride status that says
what it means in words, the stop confirmation a rider can back out of,
and the ride screen reworked around what only it can show.

The release note records that all of it was made against the simulator
and none of it has been ridden on a physical KICKR yet.
Two bugs found riding build 10 on a physical KICKR:

1. The chain-position reminder was only shown in the searching/chooser
   states, not readyCard, so it vanished the instant the trainer
   connected and the Start Shifting button jumped upward to fill the
   space. It now renders in a fixed position across every startup
   state.

2. DemoModeView.shiftButton constructed ShiftButton without the
   isProminent parameter added for the ride screen's easier/harder
   weight differentiation, so demo mode still showed two equally
   prominent buttons. It now passes isProminent: !easier to match.

Test-first: added testTheChainReminderNeverAppearsOrDisappearsAcrossStartupStates
and testDemoShiftButtonsAreDrawnLikeTheRideScreensAreWithDistinctWeight
(the latter introduces pixel-sampling averageColor/colorDistance helpers
since button styling isn't exposed via the accessibility tree), confirmed
both red, then fixed. Full suite: 30/30 UI tests, 259 core tests, 90.80%
coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Copilot AI lite review requested due to automatic review settings August 16, 2026 05:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the startup/ride UX to restore an explicit “Start Shifting” step, improves ride-status and equipment-status visibility (including accessibility layouts), adds deterministic UI regression coverage, and tightens CI by enforcing ≥90% core line coverage.

Changes:

  • Reworked startup + ride presentation: proxy advertising can start as soon as the trainer is ready; shifting is explicitly started by the rider; ride status is moved to a readable banner near the gear readout; stop confirmation uses an alert with a visible Cancel.
  • Added/expanded automated testing: new UI regression suite with debug fixtures + new/updated core unit tests for presentation rules and lifecycle behavior.
  • Updated product documentation and release copy across README + docs + App Store notes; CI now runs coverage and UI tests.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
VirtualGearsUITests/VirtualGearsUITests.swift Adds deterministic UI regression suite for startup/ride/demo flows, layout, and accessibility identifiers.
VirtualGearsProduct/VirtualGearsHomeView.swift Restores explicit “Start Shifting” flow; makes trainer proxy available when ready; reworks ride status + equipment footer UI and stop confirmation.
VirtualGearsProduct/VirtualGearsApp.swift Expands screenshot/debug fixtures to cover new UI test scenarios and accessibility dynamic type staging.
VirtualGearsProduct/SetupView.swift Improves Settings copy/labels, gear summary presentation, cassette disambiguation for VoiceOver, and adds screen accessibility identifiers.
VirtualGearsProduct/FTMSPeripheral.swift Adds a debug staging helper for “advertising but not connected” screenshot/test scenarios.
VirtualGearsProduct/ClickCentralService.swift Tracks “accepted” physical Click presses for UI feedback and adds debug staging for UI tests.
VirtualGears.xcodeproj/project.pbxproj Adds a UI test target and bumps build number to 10.
Tests/VirtualGearsCoreTests/RideReadoutPresentationTests.swift Pins down rail marker semantics and readout sizing behavior with unit tests.
Tests/VirtualGearsCoreTests/ProxyCoordinatorTests.swift Adds coverage for making the trainer proxy available before shifting begins.
Tests/VirtualGearsCoreTests/ProductPresentationTests.swift Adds/extends unit tests for plain-language connection labels and related presentation invariants.
Tests/VirtualGearsCoreTests/HeadwindProtocolTests.swift Expands protocol and decoding tests; validates mode labeling/control semantics.
Tests/VirtualGearsCoreTests/AppConfigurationTests.swift Adds tests for headwind-forget behavior, virtual gear summary, and real drivetrain naming/range descriptions.
Sources/VirtualGearsCore/ProxyCoordinator.swift Introduces makeTrainerProxyAvailable() and stages additional debug states for screenshots.
Sources/VirtualGearsCore/GearRailPresentation.swift Adds rail marker model + readout metrics logic shared by presentation and tests.
Sources/VirtualGearsCore/DrivetrainCatalog.swift Adds cassette qualifiedName to disambiguate repeated names (UI + VoiceOver).
Sources/VirtualGearsCore/AppConfiguration.swift Adjusts gear summary wording for virtual gears (“extra-low climbing range”).
README.md Updates onboarding instructions to reflect explicit “Start Shifting” step and persistent equipment statuses.
docs/requirements.md Updates usage flow and Click behavior notes to match the new startup/shifting model.
docs/index.md Updates marketing copy to reflect explicit shifting start and Click-press mirroring behavior.
docs/demo.md Updates demo explanation/captions to match new startup/shifting responsibilities.
docs/APP_STORE.md Updates App Store copy and build notes through TestFlight build 1.0 (10).
docs/accessibility.md Updates accessibility documentation for equipment status reflow and Click-press mirroring.
DEVELOPMENT.md Documents coverage requirements and how to run the UI regression suite locally.
.github/workflows/ci.yml Enables code coverage with a ≥90% gate and adds a macOS UI test job.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +438 to +462
private func averageColor(of element: XCUIElement) -> (r: Double, g: Double, b: Double) {
let screenshot = element.screenshot().image
guard let cgImage = screenshot.cgImage else { return (0, 0, 0) }
let width = cgImage.width
let height = cgImage.height
guard width > 0, height > 0,
let data = cgImage.dataProvider?.data,
let pointer = CFDataGetBytePtr(data) else { return (0, 0, 0) }
let bytesPerPixel = cgImage.bitsPerPixel / 8
let bytesPerRow = cgImage.bytesPerRow
var totals = (r: 0.0, g: 0.0, b: 0.0)
var samples = 0.0
// Sample a sparse grid rather than every pixel: fast, and averages out
// the icon/text drawn on top of the button's own fill colour.
let strideStep = max(1, min(width, height) / 12)
for y in stride(from: 0, to: height, by: strideStep) {
for x in stride(from: 0, to: width, by: strideStep) {
let offset = y * bytesPerRow + x * bytesPerPixel
guard offset + 2 < CFDataGetLength(data) else { continue }
totals.r += Double(pointer[offset])
totals.g += Double(pointer[offset + 1])
totals.b += Double(pointer[offset + 2])
samples += 1
}
}
@sbroenne
sbroenne merged commit dd62b24 into main Aug 16, 2026
6 checks passed
@sbroenne
sbroenne deleted the sbroenne-click-button-visual-feedback branch August 16, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants