Skip to content

ios: deplete the Dynamic Island rest ring instead of a static on/off ring - #81

Open
willchan wants to merge 1 commit into
mainfrom
claude/dynamic-island-icon-color-1bgm6a
Open

ios: deplete the Dynamic Island rest ring instead of a static on/off ring#81
willchan wants to merge 1 commit into
mainfrom
claude/dynamic-island-icon-color-1bgm6a

Conversation

@willchan

Copy link
Copy Markdown
Owner

What

The Dynamic Island's compact/minimal orange ring around the app icon used to just mean "resting: yes" — a static full ring for the whole rest period, whether there were 90 seconds or 5 seconds left. It now depletes clockwise over the rest period: full right as rest starts, empty right as the next set is due.

This matters most in minimal, the one Dynamic Island presentation with no text slot at all (it kicks in once a second Live Activity, e.g. Audible, is also active) — the ring there is the only signal, so making it convey progress instead of a flat yes/no gets more information into the same one glyph.

How

  • src/native/liveActivity.ts — added restStartTime alongside restEndTime to WorkoutActivityState/toContentState() (now exported for direct unit testing, matching the rest of this codebase's pure-function testing convention). A depleting ring needs both ends of a fixed date range; restEndTime alone isn't enough.
  • src/ui/workout.tssyncLiveActivity() now takes the TimerState itself (rather than just its end time) so both restStartTime/restEndTime are always derived and set together; also threaded through the mount-time timer-recovery path (browser tab resumed with a rest timer already running).
  • LiveActivityWidget.swiftbrandedIcon() draws the ring via ProgressView(timerInterval:).progressViewStyle(.circular) instead of a static strokeBorder. This is the same "hand SwiftUI a fixed date range once, let the system animate it continuously" mechanism Text(timerInterval:) already uses for the digits — no extra Live Activity pushes needed once rest starts. Added restStartDate()/restProgressRange() guards against the same "Range requires lowerBound <= upperBound" trap restEndDate() already guards against for the digits.

Notes

  • The range-validation logic (restProgressRange) would belong in ios/App/AppLogic per CLAUDE.md's "new Swift logic goes in AppLogic, with a test," but AppLogic isn't currently linked into the LiveActivityWidget extension target (only App is), and adding that link is an Xcode-GUI-only step per ios/MANUAL_SETUP.md — not scriptable from this session. It lives inline next to restEndDate(), which already has the same constraint.
  • Went through an adversarial code-review pass (two rounds): the first caught that the ring, drawn via ProgressView in .background, had no explicit frame and could render at the control's intrinsic size instead of hugging the icon — fixed by giving it an explicit .frame() and deduplicating the two now-identical ring branches into one.

Testing

  • bun run typecheck / bun run lint clean.
  • Full Playwright suite (bunx playwright test --project=chromium): 284 passed, including two new tests for toContentState()'s restStartTime handling.
  • Swift changes can't be compiled/tested locally (no Xcode/macOS in this session) — relying on the iOS CI workflow's Simulator build to catch any compile issues.

🤖 Generated with Claude Code


Generated by Claude Code

…ring
The compact/minimal Dynamic Island icon's orange ring used to just mean
"resting: yes" — a static full ring for the whole rest period, same
regardless of 5 seconds or 90 seconds left. It's now a
ProgressView(timerInterval:) that the system animates continuously on
its own: full right as rest starts, empty right as the next set is
due. That matters most in `minimal`, where the ring is the only glyph
shown at all (no digits) once a second Live Activity is also active.
- src/native/liveActivity.ts: add restStartTime alongside restEndTime
in WorkoutActivityState/toContentState (now exported for direct
testing), so the widget has both ends of a fixed date range.
- src/ui/workout.ts: thread restStartTime through syncLiveActivity()
(now takes the TimerState itself, not just its end time) and the
mount-time timer-recovery path.
- LiveActivityWidget.swift: brandedIcon() draws the ring via
ProgressView(timerInterval:).progressViewStyle(.circular) instead of
a static strokeBorder; restStartDate()/restProgressRange() guard
against the same "Range requires lowerBound <= upperBound" trap
restEndDate() already guards for the digits.
The range-validation logic would belong in ios/App/AppLogic per
CLAUDE.md, but AppLogic isn't linked into the LiveActivityWidget
extension target (only App is), and adding that link is an
Xcode-GUI-only step — same constraint the existing restEndDate() next
to it already lives with.
Sign up for freeto 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

@willchan@claude