mycode/myApp/Keging/IOS/LiveActivityManager.swift
songyc macbook 32737d74fd feat(keging): Live Activity 자가 렌더링 재설계 — 앱이 정지해도 움직이는 DI/잠금화면 (실기기 4차 피드백)
- ActivityKit엔 미래 상태 예약 API가 없음을 확인(푸시 서버 없이는 국면 갱신이 앱
  런타임 의존) → 상태에 세트 전체 일정을 실어 시스템 렌더 요소로 심장을 재구성:
  국면 색 타임라인 바(하드 스톱 그라데이션) + 실시간 플레이헤드
  ProgressView(timerInterval:) + 전체 남은 시간. 플레이헤드가 걸친 색 = 지금 국면
- 국면 라벨·횟수는 앱 갱신 유지(syncCurrent 단일 진입점), stale 시 흐림
- 오디오 키퍼: 완전 무음 대신 ±2 LSB 디더 노이즈(무음 감지 정지 회피) +
  백그라운드 전환 ~30초 브리지 태스크
- 통계 타임라인 점별 시각 라벨 제거(겹침 — 시각은 오늘 기록 목록에서)
- 도움말·번역 갱신, CLAUDE.md 현행화. 시뮬: DI 백그라운드 80초+(10/20) 진행 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-09-02 16:26:29 +09:00

69 lines
2.7 KiB
Swift

//
// LiveActivityManager.swift
// Keging
//
// Live Activity
// ( ·· §3.4).
// + (2) syncCurrent
// . staleDate( +8) .
//
import ActivityKit
import Foundation
@MainActor
final class LiveActivityManager {
static let shared = LiveActivityManager()
private var activity: Activity<KegingActivityAttributes>?
private var lastState: KegingActivityAttributes.ContentState?
private init() {}
/// Live Activity
/// ( )
func cleanupStaleActivities() {
for stale in Activity<KegingActivityAttributes>.activities {
Task { await stale.end(nil, dismissalPolicy: .immediate) }
}
activity = nil
lastState = nil
}
/// , .
///
func syncCurrent() {
let engine = KegelEngine.shared
guard case let .running(phase, rep, phaseStart, phaseEnd) = engine.state else { return }
let config = engine.config
let state = KegingActivityAttributes.ContentState(
phase: phase, rep: rep, totalReps: config.reps,
phaseStart: phaseStart, phaseEnd: phaseEnd,
sessionStart: engine.sessionStart,
prepSeconds: config.prepSeconds,
contractSeconds: config.contractSeconds,
relaxSeconds: config.relaxSeconds
)
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: engine.sessionStart),
content: content
)
}
}
func end() {
lastState = nil
guard let activity else { return }
self.activity = nil
Task { await activity.end(nil, dismissalPolicy: .immediate) }
}
}