Repository navigation
widgets: make the lock-screen accessory read like a lock-screen widget - #1715
Merged
Merged
Conversation
A community request asked for a WHOOP-style mini lock-screen widget. iOS already has one — the accessoryRectangular family is declared and renders Charge, Effort and Rest from the same snapshot — so nothing needed building. What it needed was to behave like a lock-screen widget. Two things. It tinted unconditionally. Lock-screen widgets render in .vibrant or .accented, where the system desaturates whatever colour it is handed and maps it onto the wallpaper, so a domain colour does not survive as that colour: it lands as an arbitrary grey whose luminance nobody chose. Charge, Effort and Rest were therefore neither distinguishable from each other nor reliably legible — the tint was doing no work and taking the hierarchy with it. The cells now read the widgetRenderingMode environment and hand the system .primary and .secondary off the lock screen, which are the levels it is built to map, while the home screen and the gallery keep the domain colours exactly as before. And it spent a row on its own name. This family gets roughly 72pt of height for everything, and a "NOOP" title used a whole row of it restating which widget the user had chosen to add, squeezing the three scores that are the only reason to add it. The title is gone and the heart-rate line is now conditional, so an install with no live HR gives the scores the entire area. Left alone deliberately: the metric ORDER. iOS renders Charge, Effort, Rest in the accessory and in the small/medium/large rings, and its gallery description says so; Android renders Rest, Charge, Effort in both of its widgets, and its descriptions say so. Each platform is internally consistent and self-describing, so reordering only this one view would break iOS against itself and contradict its own copy. That divergence is real and worth settling, but it is a platform-wide convention question rather than something to slip into a lock-screen polish. Also unchanged: Android cannot host this at all. NoopCompactGlanceWidget already draws the same three icon cells, but Android removed third-party lock-screen widgets in 5.0 and widgetCategory="keyguard" has been inert since, so there is no surface to target on a phone. StrandiOSWidgets is app-target, and SwiftUI does not exist on Linux, so this could not be compiled locally at all — app-build is the first compiler to see it. The AnyShapeStyle branches are spelled with explicit types rather than leading-dot inference for that reason.
The request was for a WHOOP-style mini widget, and the thing that makes theirs readable at a glance is that each cell is a GLYPH, not a word. The accessory now shows an icon over each value — figure.mind.and.body, figure.strengthtraining.traditional, moon.fill for Charge, Effort and Rest — centred, replacing the 9pt word that sat under each number. That also buys back height rather than spending it. This family gets roughly 72pt for everything, and a word under every number was the second thing naming a metric the icon already names. Accessibility is the part an icon quietly breaks, so it is handled rather than assumed: a glyph says nothing to VoiceOver, and the word it replaced was the only thing identifying the metric. Each cell collapses to a single element that still speaks "Charge, 68 percent", with an existing catalogue string for the empty case. Lock screen only, deliberately. The home-screen families draw score RINGS whose order and shape deliberately mirror TodayView's hero trio, so putting glyphs there would diverge the widget from the app's own home screen to chase a lock-screen look. Those are untouched. SF Symbols rather than porting Android's ic_widget_* drawables: they render correctly in the vibrant mode the previous commit started respecting, scale with Dynamic Type, and add no assets. The cost is that they are not pixel-identical to Android's glyphs, which is a visible-in-screenshots difference rather than a functional one. The deployment target for this extension is iOS 17, so all three symbols exist without availability guards. i18n audit clean — "No data" was already in the catalogue in all ten locales.
Re-review. Two defects in the icon commit. The accessibility empty-state used String(localized: "No data"). That compiles, passes the i18n audit because the key does exist in the catalogue, and does not work: this extension's sources are StrandiOSWidgets and StrandiOSShared only, so Strand/Resources/Localizable.xcstrings is not in the target, and String(localized:) resolves against Bundle.main — which for an app extension is the extension's own bundle. It would have rendered English in every locale while looking localized in review. It is the only String(localized:) in the whole extension; every other string here is a bare literal, because the widget is not localized at all. Now a literal too, with the reason recorded so the next person does not re-add it. Localizing the widget properly is a separate piece of work — it would mean putting the catalogue in the extension target and covering Charge, Effort and Rest as well, not one accessibility value. The other is prose that outlived its code: the doc for accessoryScore still said the value reads at full strength "and the label recedes", describing the 9pt word the same commit had replaced with a glyph. Corrected, and while there noted that the .fullColor branch is defensive rather than hot — this family renders vibrant on the lock screen and in StandBy, so the tint realistically only reaches a gallery preview. i18n audit clean.
Third review pass, all documentation and one dead argument. The doc for accessoryScore opened with "the tint applies on the home screen and in the gallery" and closed, two paragraphs later, by explaining that this family never renders on the home screen at all. Both sentences were mine, added in different commits, and together they told the reader the opposite things about the same branch. The opening now says what the closing paragraph established: the tint survives only where the system renders full colour, which for this family means a gallery preview. The type-level doc still described lock-screen accessories as "compact single-line / gauge layouts", which stopped covering the rectangular one when it became a glyph-over-value trio. And VStack(alignment: .leading) is gone. Both children set their own frame across the full width, so the alignment had no effect and only implied a left edge that is not there — the cells are centred. No behaviour change beyond that removal, which had none.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A community request asked for a WHOOP-style mini lock-screen widget. iOS already had one —
accessoryRectangularis declared and renders Charge, Effort and Rest from the same snapshot — so nothing needed building. It needed to behave like a lock-screen widget, and to look like the thing that was asked for.Before / after
1. It tinted unconditionally
Lock-screen widgets render in
.vibrantor.accented, where the system desaturates whatever colour it is handed and maps it onto the wallpaper. A domain colour does not survive as that colour — it lands as an arbitrary grey whose luminance nobody chose. So Charge, Effort and Rest were neither distinguishable from each other nor reliably legible: the tint was doing no work and taking the visual hierarchy with it.The cells now read
widgetRenderingModeand hand the system.primary/.secondaryoff the lock screen, which are the two levels it is built to map. The home screen and the widget gallery keep the domain colours exactly as before.2. It spent a row on its own name
This family gets roughly 72pt of height for everything, and a
"NOOP"title used a whole row of it restating which widget the user had just chosen to add — squeezing the three scores that are the only reason to add it. The title is gone and the heart-rate line is now conditional, so an install with no live HR gives the scores the entire area.3. Metric glyphs
What makes WHOOP's readable at a glance is that each cell is a glyph, not a word. Icons now sit over each value —
figure.mind.and.body,figure.strengthtraining.traditional,moon.fill— centred, replacing the 9pt word under each number, which was the second thing naming a metric the icon already names.Accessibility is the part an icon quietly breaks. A glyph says nothing to VoiceOver, and the word it replaced was the only thing identifying the metric. Each cell collapses to a single element that still speaks "Charge, 68 percent", using a catalogue string for the empty case —
"No data"already exists in all ten locales, so there is no new copy and the i18n gate stays clean.SF Symbols rather than porting Android's
ic_widget_*drawables: they render correctly in the vibrant mode item 1 started respecting, scale with Dynamic Type, and add no assets. The cost is that they are not pixel-identical to Android's glyphs — visible if someone screenshots both, not functional.Deliberately not done
The home-screen families. They draw score rings whose order and shape deliberately mirror TodayView's hero trio. Putting glyphs there would diverge the widget from the app's own home screen in order to chase a lock-screen look. Untouched.
The metric order. iOS renders Charge → Effort → Rest in the accessory and in the small/medium/large rings, and its gallery description says so. Android renders Rest → Charge → Effort in both of its widgets, and its descriptions say so. Each platform is internally consistent and self-describing, so reordering only this one view would break iOS against itself and contradict its own copy. That divergence is real and worth settling — as its own issue, not inside a lock-screen polish.
Android.
NoopCompactGlanceWidgetalready draws the same three icon cells, but Android removed third-party lock-screen widgets in 5.0 andwidgetCategory="keyguard"has been inert since. There is no surface on a phone to target.Verified
StrandiOSWidgetsis app-target and SwiftUI does not exist on Linux, so nothing here could be compiled locally — app-build is the first compiler to see it, and it is green on both legs for this exact head. TheAnyShapeStylebranches are written with explicit types rather than leading-dot inference for that reason: that construct type-checks in isolation and fails in a real build.Tools/i18n_audit.py --ciclean.