// // LiveActivityManager.swift // Keging // // 다이나믹 아일랜드 Live Activity — 현재 횟수·수축/이완 국면 표시. // 국면 전환 콜백 + 하트비트(2초)가 모두 sync를 부르지만 상태가 실제로 바뀐 경우에만 // 갱신을 보낸다. staleDate를 국면 종료+8초로 걸어, 앱이 정지해 갱신이 끊기면 // 시스템이 stale로 표시(위젯 쪽에서 흐리게 처리)하게 한다. // import ActivityKit import Foundation @MainActor final class LiveActivityManager { static let shared = LiveActivityManager() private var activity: Activity? private var lastState: KegingActivityAttributes.ContentState? private init() {} /// 앱 시작 시 호출 — 강제 종료 등으로 남은 이전 Live Activity를 정리한다 /// (안 하면 운동 중 강제 종료 시 다이나믹 아일랜드가 몇 시간 동안 유령으로 남음) func cleanupStaleActivities() { for stale in Activity.activities { Task { await stale.end(nil, dismissalPolicy: .immediate) } } activity = nil lastState = nil } /// 없으면 시작, 있으면 갱신 — 같은 상태면 아무것도 하지 않아 하트비트에서 불러도 안전 func sync(phase: KegelPhase, rep: Int, totalReps: Int, phaseStart: Date, phaseEnd: Date) { let state = KegingActivityAttributes.ContentState( phase: phase, rep: rep, totalReps: totalReps, phaseStart: phaseStart, phaseEnd: phaseEnd ) let content = ActivityContent(state: state, staleDate: phaseEnd.addingTimeInterval(8)) if let activity { guard state != lastState else { return } lastState = state Task { await activity.update(content) } } else { guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } lastState = state activity = try? Activity.request( attributes: KegingActivityAttributes(startedAt: Date()), content: content ) } } /// 하트비트용 — 엔진의 현재 상태를 그대로 반영 (놓친 국면 전환 자가 치유) func syncCurrent() { guard case let .running(phase, rep, phaseStart, phaseEnd) = KegelEngine.shared.state else { return } sync( phase: phase, rep: rep, totalReps: KegelEngine.shared.config.reps, phaseStart: phaseStart, phaseEnd: phaseEnd ) } func end() { lastState = nil guard let activity else { return } self.activity = nil Task { await activity.end(nil, dismissalPolicy: .immediate) } } }