// // LiveActivityManager.swift // Haru_Danim // // 추적 상태를 다이나믹 아일랜드/잠금화면 Live Activity에 반영 (CLAUDE.md §4.1, §4.2) // import Foundation import ActivityKit import SwiftData enum LiveActivityManager { /// 백그라운드(워치 명령으로 깨어난 상태 등)에서는 시스템이 Live Activity '시작'을 /// 허용하지 않으므로, 실패를 기억해 두고 앱이 포그라운드로 돌아올 때 재시도한다 static var pendingStartRetry = false /// 현재 진행 중인 세션들을 읽어 Live Activity를 시작/갱신/종료한다. /// 세션 시작·종료·수정 후마다 호출. static func sync(context: ModelContext) { let descriptor = FetchDescriptor( predicate: #Predicate { $0.endAt == nil }, sortBy: [SortDescriptor(\.startAt, order: .forward)] ) let running = (try? context.fetch(descriptor)) ?? [] // 장시간 측정 알림 예약도 같은 길목에서 동기화 (Live Activity 허용 여부와 무관) SessionAlertManager.sync(running: running) guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } let mode = LiveActivityMode( rawValue: AppGroup.defaults.string(forKey: SettingsKeys.liveActivityMode) ?? UserDefaults.standard.string(forKey: SettingsKeys.liveActivityMode) ?? "" ) ?? .latest let representative = mode == .earliest ? running.first : running.last guard let representative, let action = representative.action else { pendingStartRetry = false endAll() return } let state = TrackingActivityAttributes.ContentState( actionName: action.name, symbolName: action.symbolName, colorHex: action.sortedTags.first?.colorHex ?? "#2F6B4F", startedAt: representative.startAt, extraCount: max(running.count - 1, 0) ) let content = ActivityContent(state: state, staleDate: nil) Task { let activities = Activity.activities if let activity = activities.first { await activity.update(content) pendingStartRetry = false // 혹시 중복 생성된 것이 있으면 정리 for extra in activities.dropFirst() { await extra.end(nil, dismissalPolicy: .immediate) } } else { do { _ = try Activity.request( attributes: TrackingActivityAttributes(), content: content ) pendingStartRetry = false } catch { // 워치 명령 등 백그라운드 컨텍스트에서는 시작이 거부된다(visibility) // → 포그라운드 복귀 시 syncIfRetryNeeded가 다시 시도 pendingStartRetry = true } } } } /// 앱이 포그라운드로 돌아왔을 때, 백그라운드에서 거부됐던 시작을 재시도 static func syncIfRetryNeeded(context: ModelContext) { guard pendingStartRetry else { return } sync(context: context) } static func endAll() { Task { for activity in Activity.activities { await activity.end(nil, dismissalPolicy: .immediate) } } } }