Redesign workout live activity with circular timer ring, phase icons, and smoother updates
Some checks failed
CI / TypeScript (pull_request) Failing after 19s
CI / ESLint (pull_request) Failing after 4s
CI / Tests (pull_request) Failing after 7s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m9s
CI / Deploy Edge Functions (pull_request) Has been skipped

- Add CountdownRing with real-time arc progress on lock screen
- Replace generic dots with phase-specific SF Symbols (flame, snowflake, etc.)
- Remove horizontal progress bar in favor of round counter text
- Increase Dynamic Island expanded font sizes for better visibility
- Increase live activity sync frequency from 5s to 1s for smoother arc updates
- Add pause/resume button via TogglePauseIntent AppIntent
- Remove AlertConfiguration to silence notification sounds on updates
This commit is contained in:
Millian Lamiaux
2026-05-17 00:43:01 +02:00
parent dc3ff15e81
commit 67e2bdc8c3
3 changed files with 271 additions and 134 deletions

View File

@@ -40,6 +40,7 @@ struct WorkoutActivityAttributes: ActivityAttributes {
var exerciseName: String
var phase: WorkoutPhase
var phaseEndDate: Date
var phaseDuration: TimeInterval
var roundCurrent: Int
var roundTotal: Int
var heartRate: Double

View File

@@ -365,7 +365,7 @@ final class PlayerViewModel: ObservableObject {
// Dynamic Island / Live Activity
func syncActivity() {
func syncActivity(shouldAlert: Bool = false) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
guard isRunning else { return }
@@ -377,6 +377,7 @@ final class PlayerViewModel: ObservableObject {
exerciseName: currentExercise?.nameEn ?? "",
phase: workoutPhase,
phaseEndDate: phaseEnd,
phaseDuration: Double(totalPhaseTime),
roundCurrent: currentRound,
roundTotal: totalRoundsInBlock,
heartRate: heartRate,
@@ -393,7 +394,7 @@ final class PlayerViewModel: ObservableObject {
if let existing = workoutActivity {
nonisolated(unsafe) let safeExisting = existing
let staleDate = Date().addingTimeInterval(isPaused ? 3600 : 120)
let alert = alertForPhase(workoutPhase)
let alert = shouldAlert ? alertForPhase(workoutPhase) : nil
Task { @MainActor in
if let alert {
await safeExisting.update(ActivityContent(state: state, staleDate: staleDate), alertConfiguration: alert)
@@ -444,6 +445,7 @@ final class PlayerViewModel: ObservableObject {
exerciseName: safeActivity.content.state.exerciseName,
phase: .complete,
phaseEndDate: safeActivity.content.state.phaseEndDate,
phaseDuration: safeActivity.content.state.phaseDuration,
roundCurrent: safeActivity.content.state.roundCurrent,
roundTotal: safeActivity.content.state.roundTotal,
heartRate: safeActivity.content.state.heartRate,
@@ -457,7 +459,7 @@ final class PlayerViewModel: ObservableObject {
private func startActivitySyncTimer() {
stopActivitySyncTimer()
activitySyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
activitySyncTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
Task { @MainActor [weak self] in
self?.syncActivity()
}