mycode/myApp/HaruDanim/IOS/Core/LiveActivityManager.swift
songyc macbook e1537b32f3 feat(widget): stop button on Live Activity lock-screen banner
- 잠금화면 실시간 현황(라이브 액티비티)에 종료 수단이 없다는 보고 → 배너 오른쪽에 노란 정지 버튼 추가. 워치·⑥ 위젯과 같은 검증된 경로(NowRunningStopButton → RunActionIntent 토글) 재사용이라 위험 낮음 판단 후 진행
- ContentState에 actionID(optional) 추가 — 앱 업데이트 전 인플라이트 액티비티(키 없음)도 안전 디코딩(WatchSnapshot 선례), 없으면 버튼 없이 기존 표시
- LockScreenActivityView를 NowRunningWidget.swift로 이동 — @main 파일은 앱 타깃에서 제외라 앱 내 위젯 미리보기(-widgetPreview)에서 배너를 렌더하기 위함(동작 불변). 미리보기 최상단에 배너 박스 신설
- 다이나믹 아일랜드 스모크 확인(상태 필드 추가 회귀 없음 — 콤팩트 정상 표시). 실기기 확인 항목: 잠금화면에서 정지 버튼 탭 시 즉시 종료
- 도움말 '다이나믹 아일랜드·잠금화면' 문구에 정지 버튼 안내 추가(ko/en/ja, stale 정리), CLAUDE.md §9 갱신. Debug/Store 빌드 성공, 카탈로그 9종 클린

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp
2026-07-19 04:46:38 +09:00

92 lines
3.6 KiB
Swift

//
// 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<TimeSession>(
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),
actionID: action.uuid.uuidString
)
let content = ActivityContent(state: state, staleDate: nil)
Task {
let activities = Activity<TrackingActivityAttributes>.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<TrackingActivityAttributes>.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<TrackingActivityAttributes>.activities {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
}
}