feat: redesign player with Dynamic Island, compact timer, and fix Live Activity timer drift #2
Reference in New Issue
Block a user
Delete Branch "revamp-timer-video-layout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
WorkoutLiveActivityandMusicLiveActivitywidgets — shows exercise name, countdown timer, round progress, heart rate, and music trackphaseEndDaterecalculationRoot Cause (Timer Drift)
The previous implementation recalculated
phaseEndDate = Date() + timeRemainingon every call toupdateWorkoutActivity()— which fired from 5 different triggers (phase change, running state, paused state, track change, every 5 seconds). Each recalculation shifted the target date slightly, causing the Dynamic Island'sText(timerInterval:)display to jump or drift relative to the in-app timer.A second bug caused premature Live Activity creation:
updateWorkoutActivity()was called whenmusicVM.currentTrackloaded (before the user tapped play), storing aphaseEndDatebased on the prep phase's 5-second duration. When the workout actually started seconds later, the stored stale date was reused, producing a timer at 0.Fix
phaseEndDateas@State— calculated once per phase viaphaseEndDate = nilon phase change and resume, then locked for subsequent updatesupdateWorkoutActivity()behindvm.isRunning— prevents Live Activity from being created or updated before the workout startsFiles Changed
PlayerView.swiftPlayerViewModel.swiftWorkoutLiveActivity.swiftMusicLiveActivity.swiftWorkoutActivityAttributes.swiftMusicActivityAttributes.swiftTheme.swiftMusicService.swiftHomeTab.swiftZoneHighlightIcon.swiftTabataGoApp.swiftInfo.plistLocalizable.xcstringsTest Coverage Report
Coverage summary not available.
**Architecture (PlayerViewModel):** - Move ActivityKit lifecycle from SwiftUI View to ViewModel (MVVM correction) - call syncActivity() at END of enterPhase() — after all state is set, eliminating the race where phase was Published before timeRemaining - Always recalculate phaseEndDate = Date() + timeRemaining (no stale cache) - Dedicated Timer in ViewModel for periodic heart-rate/track sync (5s) - Start/stop activity sync timer on play/pause/resume/abandon/finish - stale activity reference discard + recreate-on-failure fallback - Modern iOS 16.2+ API: ActivityContent, non-throwing update() **PlayerView:** - Remove all ActivityKit code (import, @State workoutActivity, phaseEndDate, dynamicIslandAvailable, 4 methods, .onReceive timer) - Delegate to ViewModel: onChange(musicVM.currentTrack) sets vm.trackTitle/Artist and calls vm.syncActivity(); onDisappear calls await vm.endActivity() - Music/audio onChange handlers no longer contain activity logic **Info.plist:** - Add UIBackgroundModes → audio so music continues and app stays alive in background, allowing Timer-based activity updates - Widget Info.plist: add NSSupportsLiveActivitiesFrequentUpdates **WorkoutActivityAttributes.ContentState:** - Add Sendable conformance for Swift 6 strict concurrency Fixes: timer stuck at 0 on first work phase, exercise name missing, music stopping in background, Dynamic Island freezing in background, widget drift due to cached phaseEndDateTest Coverage Report
Coverage summary not available.
Test Coverage Report
Coverage summary not available.
Test Coverage Report
Coverage summary not available.