Use semantic control colours, snappy animations, and guard the shimmer - #558
Merged
Merged
Conversation
bradleymackey
force-pushed
the
native-standardization-dynamic-type
branch
from
September 14, 2026 16:50
ac0af5d to
2b07fb4
Compare
Base automatically changed from
native-standardization-dynamic-type
to
main
September 14, 2026 16:50
## Reduce Motion
`Shimmer` runs `.linear(duration: 1.5).repeatForever(autoreverses: false)`
— an indefinitely repeating animation — and it is applied to every card in
the feed simultaneously while editing. There was no
`accessibilityReduceMotion` check anywhere in the codebase.
`.shimmering(active:)` now routes through a wrapper that drops the effect
entirely when Reduce Motion is on. The shimmer itself is unchanged.
The obvious alternative was to replace `Shimmer` with
`.redacted(reason: .placeholder)`, which is reduce-motion aware for free.
That is not equivalent here: `.redacted` replaces content with grey
placeholder capsules, whereas these call sites shimmer a card that is
still meant to show its own text ("is editing", "Tap to View"). Swapping
them would change what the card says, not just how it animates.
## Semantic colours for control tracks
`systemGray2`/`systemGray6` are *background-ramp* colours and were being
used as the fill behind a progress indicator. Both timer bars now use
`Color(.quaternarySystemFill)`, which is the ramp intended for controls.
`CodeTimerHorizontalBarView`'s default `color` was a literal `.blue`, and
`VaultCardModifier`'s `.prominent` background was `Color.blue`. Both are
now `.accentColor`, so they follow the app's accent instead of pinning
themselves to blue.
## Animations
All 22 `.animation(.easeOut, …)` sites become `.snappy`. `.easeOut` reads
flat and mechanical next to the system's own transitions; `.snappy` is
the current idiom. The feed's `.easeOut(duration: 0.1)` went with them —
100ms reads as a jump rather than a transition.
## Deprecations
The 16 remaining `.foregroundColor(_:)` call sites become
`.foregroundStyle(_:)`, which is its replacement and the only one of the
two that accepts hierarchical and material styles.
## Not done, deliberately
**The in-app timer bar keeps its hand-built implementation.** Converting
it to `ProgressView(value:total:)` + `.progressViewStyle(.linear)` was
planned, on the grounds that the widget already renders the same countdown
with `ProgressView(timerInterval:)`.
Two reasons not to:
- `.progressViewStyle(.linear)` draws a ~4pt hairline. These bars are a
deliberately chunky 12pt filled indicator, so the swap would be a
visual regression on the app's most-viewed screen, not a
standardization.
- `ProgressView(timerInterval:)` specifically cannot be used in-app. The
bar is driven by the injected `injector.clock` (an `EpochClockMock`
under test) so that snapshots are deterministic; `timerInterval:`
renders against wall-clock `Date` and would make every OTP snapshot
time-dependent. The widget can use it because a widget timeline is
already `Date`-based.
The accessibility value this would have bought — a free
`.accessibilityValue` on the bar — belongs with the accessibility pass,
which is out of scope for this series.
The two animation-suppression workarounds (`PlaceholderView`'s
`.transaction { $0.animation = nil }` and `BackupKeyChangeView`'s
`.animation(.none, value:)`) are also left alone. Removing them requires
re-scoping a broad `.animation(_:value:)` higher in the tree, which risks
a behaviour change for no visual gain.
## Verification
Local, iPhone 18 Pro Max / iOS 27.0:
- `xcodebuild build-for-testing` — `TEST BUILD SUCCEEDED`
- Full suite, `-parallel-testing-enabled NO` — 2834 passed, 0 failures
across 24 bundles
- 3 snapshots re-recorded (`HorizontalTimerProgressBarView`, from the
track colour) and visually reviewed
- `make format` + `make lint` — clean
⚠️ Automatic CI is still disabled (#548), so this is local verification only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
force-pushed
the
native-standardization-progress-motion
branch
from
September 14, 2026 16:51
4b93bfa to
8d7b546
Compare
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.
Reduce Motion
Shimmerruns.linear(duration: 1.5).repeatForever(autoreverses: false)— an indefinitely repeating animation — and it is applied to every card in
the feed simultaneously while editing. There was no
accessibilityReduceMotioncheck anywhere in the codebase..shimmering(active:)now routes through a wrapper that drops the effectentirely when Reduce Motion is on. The shimmer itself is unchanged.
The obvious alternative was to replace
Shimmerwith.redacted(reason: .placeholder), which is reduce-motion aware for free.That is not equivalent here:
.redactedreplaces content with greyplaceholder capsules, whereas these call sites shimmer a card that is
still meant to show its own text ("is editing", "Tap to View"). Swapping
them would change what the card says, not just how it animates.
Semantic colours for control tracks
systemGray2/systemGray6are background-ramp colours and were beingused as the fill behind a progress indicator. Both timer bars now use
Color(.quaternarySystemFill), which is the ramp intended for controls.CodeTimerHorizontalBarView's defaultcolorwas a literal.blue, andVaultCardModifier's.prominentbackground wasColor.blue. Both arenow
.accentColor, so they follow the app's accent instead of pinningthemselves to blue.
Animations
All 22
.animation(.easeOut, …)sites become.snappy..easeOutreadsflat and mechanical next to the system's own transitions;
.snappyisthe current idiom. The feed's
.easeOut(duration: 0.1)went with them —100ms reads as a jump rather than a transition.
Deprecations
The 16 remaining
.foregroundColor(_:)call sites become.foregroundStyle(_:), which is its replacement and the only one of thetwo that accepts hierarchical and material styles.
Not done, deliberately
The in-app timer bar keeps its hand-built implementation. Converting
it to
ProgressView(value:total:)+.progressViewStyle(.linear)wasplanned, on the grounds that the widget already renders the same countdown
with
ProgressView(timerInterval:).Two reasons not to:
.progressViewStyle(.linear)draws a ~4pt hairline. These bars are adeliberately chunky 12pt filled indicator, so the swap would be a
visual regression on the app's most-viewed screen, not a
standardization.
ProgressView(timerInterval:)specifically cannot be used in-app. Thebar is driven by the injected
injector.clock(anEpochClockMockunder test) so that snapshots are deterministic;
timerInterval:renders against wall-clock
Dateand would make every OTP snapshottime-dependent. The widget can use it because a widget timeline is
already
Date-based.The accessibility value this would have bought — a free
.accessibilityValueon the bar — belongs with the accessibility pass,which is out of scope for this series.
The two animation-suppression workarounds (
PlaceholderView's.transaction { $0.animation = nil }andBackupKeyChangeView's.animation(.none, value:)) are also left alone. Removing them requiresre-scoping a broad
.animation(_:value:)higher in the tree, which risksa behaviour change for no visual gain.
Verification
Local, iPhone 18 Pro Max / iOS 27.0:
xcodebuild build-for-testing—TEST BUILD SUCCEEDED-parallel-testing-enabled NO— 2834 passed, 0 failuresacross 24 bundles
HorizontalTimerProgressBarView, from thetrack colour) and visually reviewed
make format+make lint— clean🤖 Generated with Claude Code