fix: Live Activity persists after workout cancel/background
Root cause: observeActivityState() prematurely set workoutActivity=nil when the activity went .stale (e.g. app backgrounded >2 minutes). This prevented endActivity() from calling .end() on the stale activity, leaving it visible on the Lock Screen and Dynamic Island indefinitely. Fixes (all in PlayerViewModel.swift): 1. observeActivityState(): Split the monolithic stale/ended/dismissed handler. .stale now only stops the sync timer but keeps the workoutActivity reference so endActivity() can still call .end() to properly dismiss the stale Live Activity. 2. syncActivity() nil guard: Changed from != .active to explicit == .ended || == .dismissed so stale activities are not prematurely discarded when tick() re-enters syncActivity(). 3. endActivity(): Added stopActivitySyncTimer() defensive call at top to prevent orphaned timer from racing in and recreating the activity during .end(). Also relaxed the guard from == .active to != .ended && != .dismissed so stale activities can be ended. 4. abandonWorkout(): Explicitly set isRunning=false + isPaused=false before cleanup to prevent accidental Live Activity recreation.
This commit is contained in:
@@ -116,6 +116,8 @@ final class PlayerViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func abandonWorkout() {
|
||||
isRunning = false
|
||||
isPaused = false
|
||||
timer?.invalidate()
|
||||
stopActivitySyncTimer()
|
||||
Task { try? await liveSession.end() }
|
||||
@@ -401,7 +403,8 @@ final class PlayerViewModel: ObservableObject {
|
||||
phaseElapsedSeconds: max(0, Double(totalPhaseTime) - Double(timeRemaining))
|
||||
)
|
||||
|
||||
if let existing = workoutActivity, existing.activityState != .active {
|
||||
if let existing = workoutActivity,
|
||||
existing.activityState == .ended || existing.activityState == .dismissed {
|
||||
workoutActivity = nil
|
||||
}
|
||||
|
||||
@@ -449,12 +452,14 @@ final class PlayerViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
func endActivity() async {
|
||||
stopActivitySyncTimer()
|
||||
guard let activity = workoutActivity else { return }
|
||||
workoutActivity = nil
|
||||
activityStateTask?.cancel()
|
||||
activityStateTask = nil
|
||||
nonisolated(unsafe) let safeActivity = activity
|
||||
guard safeActivity.activityState == .active else { return }
|
||||
guard safeActivity.activityState != .ended,
|
||||
safeActivity.activityState != .dismissed else { return }
|
||||
let finalState = WorkoutActivityAttributes.ContentState(
|
||||
exerciseName: safeActivity.content.state.exerciseName,
|
||||
phase: .complete,
|
||||
@@ -497,7 +502,11 @@ final class PlayerViewModel: ObservableObject {
|
||||
activityStateTask = Task { @MainActor [weak self] in
|
||||
for await state in activity.activityStateUpdates {
|
||||
guard let self else { return }
|
||||
if state == .stale || state == .ended || state == .dismissed {
|
||||
if state == .stale {
|
||||
// Stop sync timer, but keep the activity reference
|
||||
// so endActivity() can still call .end() to properly dismiss it.
|
||||
self.stopActivitySyncTimer()
|
||||
} else if state == .ended || state == .dismissed {
|
||||
self.workoutActivity = nil
|
||||
self.stopActivitySyncTimer()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user