From 40ffb04f313b8df8b449e0841243a869db8972fa Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Sat, 4 Jul 2026 17:54:09 +0200 Subject: [PATCH] fix: Dynamic Island, Live Activity errors, background audio stop, CFBundleVersion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — Audio background resilience: - Remove .mixWithOthers, app takes audio focus for coach cues - Add AVAudioSession.interruptionNotification handler (re-activates after calls/Siri) - Add AVAudioSession.routeChangeNotification handler (re-activates on headphone changes) Phase 2 — Live Activity error surfacing: - PlayerViewModel: @Published liveActivityError captures auth-denied + request errors - PlayerView: translucent error banner with dismiss button (non-blocking) Phase 3 — CFBundleVersion 1→2 in all 4 Info.plist files (match project.yml CURRENT_PROJECT_VERSION: 2) --- tabatago-swift/TabataGo/Resources/Info.plist | 2 +- .../TabataGo/Services/AudioService.swift | 45 ++++++++++++++++++- .../TabataGo/ViewModels/PlayerViewModel.swift | 8 +++- .../TabataGo/Views/Player/PlayerView.swift | 34 ++++++++++++++ .../TabataGoWatch/Complications/Info.plist | 2 +- .../TabataGoWatch/Resources/Info.plist | 2 +- tabatago-swift/TabataGoWidget/Info.plist | 2 +- 7 files changed, 89 insertions(+), 6 deletions(-) diff --git a/tabatago-swift/TabataGo/Resources/Info.plist b/tabatago-swift/TabataGo/Resources/Info.plist index 9af55fc..36ae005 100644 --- a/tabatago-swift/TabataGo/Resources/Info.plist +++ b/tabatago-swift/TabataGo/Resources/Info.plist @@ -28,7 +28,7 @@ CFBundleVersion - 1 + 2 NSHealthShareUsageDescription TabataGo reads your health data to show fitness stats and personalize your workouts. NSHealthUpdateUsageDescription diff --git a/tabatago-swift/TabataGo/Services/AudioService.swift b/tabatago-swift/TabataGo/Services/AudioService.swift index 0b053a3..ec8c6b4 100644 --- a/tabatago-swift/TabataGo/Services/AudioService.swift +++ b/tabatago-swift/TabataGo/Services/AudioService.swift @@ -28,12 +28,55 @@ final class AudioService { try audioSession.setCategory( .playback, mode: .default, - options: [.mixWithOthers, .allowAirPlay, .allowBluetooth] + options: [.allowAirPlay, .allowBluetooth] ) try audioSession.setActive(true) } catch { print("[Audio] Session configuration failed: \(error)") } + + // ─── Interruption Handler + NotificationCenter.default.addObserver( + forName: AVAudioSession.interruptionNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self, + let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } + + if type == .ended { + guard let optionsValue = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt, + AVAudioSession.InterruptionOptions(rawValue: optionsValue).contains(.shouldResume) else { return } + do { + try self.audioSession.setActive(true) + } catch { + print("[Audio] Re-activation failed after interruption: \(error)") + } + } + } + + // ─── Route Change Handler + NotificationCenter.default.addObserver( + forName: AVAudioSession.routeChangeNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self, + let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } + + switch reason { + case .newDeviceAvailable, .oldDeviceUnavailable: + do { + try self.audioSession.setActive(true) + } catch { + print("[Audio] Route re-activation failed: \(error)") + } + default: + break + } + } } // ─── Phase Audio Cues ───────────────────────────────────────── diff --git a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift index 087cd8c..ec53046 100644 --- a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift +++ b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift @@ -39,6 +39,7 @@ final class PlayerViewModel: ObservableObject { @Published var isComplete: Bool = false @Published var showExitConfirmation: Bool = false @Published private(set) var completedSession: WorkoutSession? = nil + @Published var liveActivityError: String? = nil // Track info for Live Activity — set by View when music changes var currentTrackTitle = "" @@ -382,7 +383,10 @@ final class PlayerViewModel: ObservableObject { // ─── Dynamic Island / Live Activity ───────────────────────────── func syncActivity(shouldAlert: Bool = false) { - guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } + guard ActivityAuthorizationInfo().areActivitiesEnabled else { + liveActivityError = "Live Activities are disabled in Settings" + return + } guard isRunning else { return } let isPlayingMusic = (phase == .work || phase == .rest) && isRunning && !isPaused @@ -449,8 +453,10 @@ final class PlayerViewModel: ObservableObject { attributes: attrs, content: ActivityContent(state: state, staleDate: staleDate) ) + liveActivityError = nil observeActivityState() } catch { + liveActivityError = error.localizedDescription print("❌ Failed to start Live Activity: \(error.localizedDescription)") } } diff --git a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift index c43cb71..ea6a7a8 100644 --- a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift +++ b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift @@ -66,6 +66,40 @@ struct PlayerView: View { } } + // ══════════════════════════════════════════════════ + // Live Activity error banner (non-blocking) + // ══════════════════════════════════════════════════ + if let error = vm.liveActivityError { + VStack { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.caption) + .foregroundStyle(.yellow) + Text(error) + .font(.caption2.weight(.medium)) + .foregroundStyle(.white.opacity(0.9)) + Spacer() + Button { + vm.liveActivityError = nil + } label: { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white.opacity(0.6)) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(.ultraThinMaterial.opacity(0.9)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal, 16) + .padding(.top, 60) + Spacer() + } + .transition(.move(edge: .top).combined(with: .opacity)) + .animation(.spring(duration: 0.3), value: vm.liveActivityError) + .zIndex(100) + } + // ══════════════════════════════════════════════════ // Layer 4 — Compact timer ring (top-right corner) // ══════════════════════════════════════════════════ diff --git a/tabatago-swift/TabataGoWatch/Complications/Info.plist b/tabatago-swift/TabataGoWatch/Complications/Info.plist index e556090..5bfd1a7 100644 --- a/tabatago-swift/TabataGoWatch/Complications/Info.plist +++ b/tabatago-swift/TabataGoWatch/Complications/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSExtension NSExtensionPointIdentifier diff --git a/tabatago-swift/TabataGoWatch/Resources/Info.plist b/tabatago-swift/TabataGoWatch/Resources/Info.plist index 0eed266..ea4cb6f 100644 --- a/tabatago-swift/TabataGoWatch/Resources/Info.plist +++ b/tabatago-swift/TabataGoWatch/Resources/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSHealthShareUsageDescription TabataGo reads your heart rate and calories during workouts. NSHealthUpdateUsageDescription diff --git a/tabatago-swift/TabataGoWidget/Info.plist b/tabatago-swift/TabataGoWidget/Info.plist index fb382bf..b64cae1 100644 --- a/tabatago-swift/TabataGoWidget/Info.plist +++ b/tabatago-swift/TabataGoWidget/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSExtension NSExtensionPointIdentifier