- 실기기 판독: 진동은 끝까지 정시 = 앱 생존, 그러나 국면 라벨·횟수는 항상 4번째 전송(수축 2회차)에서 동결 + 진행바만 흐름 → 백그라운드 Activity.update를 시스템이 예산제로 폐기하는 것으로 확정. staleDate 재렌더도 미발화(시뮬 실측) - 설계 전환: ContentState = 세트 일정뿐, 시작 시 1회 등록(ensureStarted) 후 update 호출 0회. 화면은 시스템 렌더(국면 색 타임라인 바+플레이헤드+남은 시간)와 영원히 참인 정적 정보(수축·이완 범례, 세트 구성)만 — 동결할 데이터 자체가 없음 - 컴팩트 = 미니 타임라인+남은 시간, 미니멀 = 국면 두 색 점. 국면 라벨·횟수 텍스트는 LA에서 제거(국면=진동+플레이헤드 색, 횟수=앱) - 통계 타임라인 점별 시각 라벨 제거(겹침), 도움말·번역 갱신, CLAUDE.md 현행화 - 시뮬: 등록 후 갱신 없이 35초간 4:57→4:27 정확 진행·완주 시 DI 해제 확인 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
58 lines
2.2 KiB
Swift
58 lines
2.2 KiB
Swift
//
|
|
// LiveActivityManager.swift
|
|
// Keging
|
|
//
|
|
// 다이나믹 아일랜드 Live Activity — 세트 일정을 실어 **시작할 때 한 번만 등록**하고
|
|
// 이후 갱신하지 않는다(종료·중단 시 dismiss만). 위젯이 전부 시스템 렌더 요소로 그리는
|
|
// 구조라(§3.4) 갱신이 필요 없고, 백그라운드 갱신 예산제(실기기 실측: 4번째부터 버려짐)의
|
|
// 영향도 받지 않는다.
|
|
//
|
|
|
|
import ActivityKit
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class LiveActivityManager {
|
|
static let shared = LiveActivityManager()
|
|
|
|
private var activity: Activity<KegingActivityAttributes>?
|
|
|
|
private init() {}
|
|
|
|
/// 앱 시작 시 호출 — 강제 종료 등으로 남은 이전 Live Activity를 정리한다
|
|
/// (안 하면 운동 중 강제 종료 시 다이나믹 아일랜드가 몇 시간 동안 유령으로 남음)
|
|
func cleanupStaleActivities() {
|
|
for stale in Activity<KegingActivityAttributes>.activities {
|
|
Task { await stale.end(nil, dismissalPolicy: .immediate) }
|
|
}
|
|
activity = nil
|
|
}
|
|
|
|
/// 운동 중인데 Live Activity가 없으면 등록 — 시작 직후 1회가 정상 경로,
|
|
/// 하트비트에서도 불러 등록 실패(권한 등)를 재시도한다. 이미 있으면 아무것도 안 함
|
|
func ensureStarted() {
|
|
guard activity == nil else { return }
|
|
let engine = KegelEngine.shared
|
|
guard case .running = engine.state else { return }
|
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
let config = engine.config
|
|
let state = KegingActivityAttributes.ContentState(
|
|
sessionStart: engine.sessionStart,
|
|
prepSeconds: config.prepSeconds,
|
|
contractSeconds: config.contractSeconds,
|
|
relaxSeconds: config.relaxSeconds,
|
|
totalReps: config.reps
|
|
)
|
|
activity = try? Activity.request(
|
|
attributes: KegingActivityAttributes(startedAt: engine.sessionStart),
|
|
content: ActivityContent(state: state, staleDate: nil)
|
|
)
|
|
}
|
|
|
|
func end() {
|
|
guard let activity else { return }
|
|
self.activity = nil
|
|
Task { await activity.end(nil, dismissalPolicy: .immediate) }
|
|
}
|
|
}
|