Skip to content

widgets: give the home-screen vitals footer the app's own HR/HRV icons - #1798

Merged
ryanbr merged 3 commits into
mainfrom
fix/1795-widget-vitals-icons
Sep 2, 2026
Merged

ryanbr merged 3 commits into
mainfrom
fix/1795-widget-vitals-icons

Conversation

@ryanbr

@ryanbr ryanbr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reported in #1795 by @Sneheth, who is right: in the home-screen widget footer the HR and HRV
values read backwards against their icons.

The bug

vitalsFooter paired snap.bpm with waveform.path.ecg and snap.hrv with heart.fill —
the inverse of the pairing used everywhere else in the app:

surface HRV HR / resting HR
TodayView:2816-2820 (adjacent rows) waveform.path.ecg heart.fill
TodayCustomizationMetadata:44-45 waveform.path.ecg heart.fill
DashboardCards:88-89 waveform.path.ecg heart.fill
MetricCatalog:134 waveform.path.ecg —
NOOPLiveActivity:39 (same extension) — heart.fill
NOOPWidget:278-281 (this footer) heart.fill waveform.path.ecg

It matters more than a mismatched glyph normally would: the footer renders no text labels, so
the icon is the only thing naming each number.

Not a regression — the footer has been this way since #1022, and it is untouched by the 11.0.0
diff. Unrelated to the other widget report going around.

Why it happened, recorded so it is not "corrected" back

waveform.path.ecg IS the right symbol for Live HR as a feature — RootView:126,
RootTabView:640 and HomeScreenQuickActions:32 all use it for the Live screen. As a metric
icon it belongs to HRV, and this footer is the single place that draws both metrics side by
side, which is where the collision becomes the whole problem. The doc comment says this.

Second defect, same four lines

The three vitals were bare numbers with no unit and no name, so VoiceOver read "58", "64",
"84 percent" — three anonymous figures. Added accessibilityLabel / accessibilityValue,
carrying spoken separately from the rendered text so the unit reaches a reader who cannot
see the glyph.

This is exactly the gap #1715 fixed for the lock-screen accessory ("an icon says nothing to
VoiceOver"). The footer predates that change and never got the treatment. Literals stay plain
rather than String(localized:), for the reason already documented on accessoryScore: this
extension does not carry the app's string catalog, so String(localized:) would look localised
and render English regardless.

Verification

  • swiftc -parse clean; brace balance checked on the touched region.
  • Tools/doc_comment_lint.py exit 0, Tools/i18n_audit.py --ci exit 0.
  • Rendering is otherwise unchanged: .labelStyle(.titleAndIcon) still propagates through the
    helper, the – placeholder and the !compact gate on HRV are preserved, and the diff
    contains nothing beyond the icon swap and the accessibility modifiers.
  • Not compile-verified. This is app-target Swift in StrandiOSWidgets, which
    swift-packages.yml does not build and app-build.yml is disabled by default. Needs an
    app-build dispatch before merge.
  • No test added, deliberately. The natural home is StrandTests, which no default CI runs,
    so a pin on the icon pairing would rot silently. The doc comment carries the reasoning
    instead. Happy to add one if you would rather have it.
  • On-device check still owed: the footer appears in systemSmall and systemMedium only.

The footer under the score rings paired heart rate with waveform.path.ecg and HRV with
heart.fill - the inverse of the pairing every other surface uses. TodayView renders the two
as adjacent rows the other way round, and DashboardCards, TodayCustomizationMetadata,
MetricCatalog and this extension's own Live Activity all agree with TodayView. The footer
has been the lone outlier since #1022.

It matters more here than a mismatched glyph usually would, because the footer shows no text
labels at all: the icon is the only thing naming each number.

The inversion was an understandable one, and the doc comment now names it so it does not get
"corrected" back. waveform.path.ecg IS the right symbol for Live HR as a feature - RootView,
RootTabView and HomeScreenQuickActions all use it for the Live screen. As a metric icon it is
HRV's, and this footer is the one place that draws both metrics side by side.

Also gives the three vitals accessibility labels and values. They were bare numbers with no
unit and no name, so VoiceOver read "58", "64", "84 percent" - three anonymous figures. This
is the same gap #1715 fixed for the lock-screen accessory ("an icon says nothing to
VoiceOver"); the footer predates that change and never got it. Literals are plain rather than
String(localized:), for the reason already documented on accessoryScore: the extension does
not carry the app's string catalog.

Reported in #1795 by @Sneheth.
Re-review: vital() set accessibilityLabel/accessibilityValue without first calling
accessibilityElement(children: .ignore), making it the only labelled graphic in this file that
does not. accessoryScore and WidgetScoreRing both collapse first.

It is not cosmetic. Label under .titleAndIcon renders an image beside a text, so without the
collapse the bare number remains its own accessibility element and whether the label wins is
SwiftUI's container semantics rather than something this file decides - and a widget cannot be
checked for that here. Matching the two sites that already work is strictly safer than relying
on how Label happens to decompose.
statCell renders the value, then the unit as a separate view, then an abbreviated caption. Read
as-is that is "64", "ms", "HRV" - three fragments in value-before-label order, with ms and bpm
spelled out letter by letter. Less severe than the footer, because the caption at least names
the metric somewhere, but still not a reading anyone would choose.

Collapses the cell like the other three labelled graphics in this file and gives it a spoken
name and unit, so it reads "Heart rate variability, 64 milliseconds". spoken falls back to the
rendered value rather than to "No data", so a caller that omits it degrades to the old reading
instead of asserting something false.
@Sneheth

Sneheth commented Sep 2, 2026 •

Copy link
Copy Markdown
IMG_0072

Also to add to this PR, I also feel the lock screen widgets are slightly misaligned if you observe the central effort component. And I also feel that HR on lock screen widget is pointless since it is not realtime anyway and offsets the entire widget look

@ryanbr
ryanbr merged commit 208c442 into main Sep 2, 2026
3 checks passed
@ryanbr
ryanbr deleted the fix/1795-widget-vitals-icons branch September 2, 2026 03:10
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