diff --git a/tabatago-swift/TabataGo/Models/MusicActivityAttributes.swift b/tabatago-swift/TabataGo/Models/MusicActivityAttributes.swift index fcd0473..5fa97df 100644 --- a/tabatago-swift/TabataGo/Models/MusicActivityAttributes.swift +++ b/tabatago-swift/TabataGo/Models/MusicActivityAttributes.swift @@ -1,7 +1,7 @@ import ActivityKit struct MusicActivityAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { + public struct ContentState: Codable, Hashable, Sendable { var title: String var artist: String var isPlaying: Bool diff --git a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift index 779c5a7..ea7ae7a 100644 --- a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift +++ b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift @@ -2,7 +2,7 @@ import Foundation import SwiftData import SwiftUI import UIKit -@preconcurrency import ActivityKit +import ActivityKit // ─── Timer Phase ────────────────────────────────────────────────── @@ -52,6 +52,7 @@ final class PlayerViewModel: ObservableObject { private var liveSession = LiveWorkoutSession() private var workoutActivity: Activity? private var activitySyncTimer: Timer? + private var activityStateTask: Task? // Warmup phase index (step through warmup movements) private var warmupIndex: Int = 0 @@ -388,8 +389,9 @@ final class PlayerViewModel: ObservableObject { } if let existing = workoutActivity { + nonisolated(unsafe) let safeExisting = existing Task { @MainActor in - await existing.update(ActivityContent(state: state, staleDate: nil)) + await safeExisting.update(ActivityContent(state: state, staleDate: Date().addingTimeInterval(120))) } } else { createOrUpdateActivity(with: state) @@ -401,29 +403,33 @@ final class PlayerViewModel: ObservableObject { do { workoutActivity = try Activity.request( attributes: attrs, - content: ActivityContent(state: state, staleDate: nil) + content: ActivityContent(state: state, staleDate: Date().addingTimeInterval(120)) ) + observeActivityState() } catch { - // Will retry on next syncActivity() call + print("❌ Failed to start Live Activity: \(error.localizedDescription)") } } func endActivity() async { guard let activity = workoutActivity else { return } workoutActivity = nil - guard activity.activityState == .active else { return } + activityStateTask?.cancel() + activityStateTask = nil + nonisolated(unsafe) let safeActivity = activity + guard safeActivity.activityState == .active else { return } let finalState = WorkoutActivityAttributes.ContentState( - exerciseName: activity.content.state.exerciseName, + exerciseName: safeActivity.content.state.exerciseName, phase: "complete", - phaseEndDate: activity.content.state.phaseEndDate, - roundCurrent: activity.content.state.roundCurrent, - roundTotal: activity.content.state.roundTotal, - heartRate: activity.content.state.heartRate, - trackTitle: activity.content.state.trackTitle, - trackArtist: activity.content.state.trackArtist, + phaseEndDate: safeActivity.content.state.phaseEndDate, + roundCurrent: safeActivity.content.state.roundCurrent, + roundTotal: safeActivity.content.state.roundTotal, + heartRate: safeActivity.content.state.heartRate, + trackTitle: safeActivity.content.state.trackTitle, + trackArtist: safeActivity.content.state.trackArtist, isPlaying: false ) - await activity.end(ActivityContent(state: finalState, staleDate: nil), dismissalPolicy: .immediate) + await safeActivity.end(ActivityContent(state: finalState, staleDate: nil), dismissalPolicy: .immediate) } private func startActivitySyncTimer() { @@ -438,5 +444,21 @@ final class PlayerViewModel: ObservableObject { private func stopActivitySyncTimer() { activitySyncTimer?.invalidate() activitySyncTimer = nil + activityStateTask?.cancel() + activityStateTask = nil + } + + private func observeActivityState() { + guard let activity = workoutActivity else { return } + activityStateTask?.cancel() + activityStateTask = Task { @MainActor [weak self] in + for await state in activity.activityStateUpdates { + guard let self else { return } + if state == .stale || state == .ended || state == .dismissed { + self.workoutActivity = nil + self.stopActivitySyncTimer() + } + } + } } }