- DataStore: App Group 컨테이너의 HaruDanim.store 사용, 기존 default.store 1회 자동 이관 - 프리미엄 + 동기화 토글 시 CloudKit(.private) 구성으로 컨테이너 생성, 실패하면 로컬 폴백 - CloudKit 요건(모든 관계 optional)에 맞춰 to-many 관계를 optional 저장 + 기존 이름 접근자로 리팩터링 (originalName 매핑으로 기존 데이터 유지) - Tag/Action/Goal/Quest에 uuid 추가 (시리 단축어·위젯·워치 식별자) + 중복 보정 - 모델 레이어(Models/Settings/DayMath/QuestProgress/Formatters/Theme)를 Shared/로 이동 — 위젯 확장과 공유 - 집계 설정(주 시작 요일·하루 시작 시간·대표 시간·짧은 기록 무시)을 App Group defaults로 이관해 위젯·워치와 공유, 설정의 '기기 간 동기화' 토글 활성화 - Goal.combinedSpanRatio 공용화 (모음 탭 카드·위젯·시리가 동일 산식 사용) - DEBUG 검증 인자: -cloudSync YES, -settingsScrollPremium YES Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
69 lines
2.4 KiB
Swift
69 lines
2.4 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 func sync(context: ModelContext) {
|
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
|
|
let descriptor = FetchDescriptor<TimeSession>(
|
|
predicate: #Predicate { $0.endAt == nil },
|
|
sortBy: [SortDescriptor(\.startAt, order: .forward)]
|
|
)
|
|
let running = (try? context.fetch(descriptor)) ?? []
|
|
|
|
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 {
|
|
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<TrackingActivityAttributes>.activities
|
|
if let activity = activities.first {
|
|
await activity.update(content)
|
|
// 혹시 중복 생성된 것이 있으면 정리
|
|
for extra in activities.dropFirst() {
|
|
await extra.end(nil, dismissalPolicy: .immediate)
|
|
}
|
|
} else {
|
|
_ = try? Activity<TrackingActivityAttributes>.request(
|
|
attributes: TrackingActivityAttributes(),
|
|
content: content
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
static func endAll() {
|
|
Task {
|
|
for activity in Activity<TrackingActivityAttributes>.activities {
|
|
await activity.end(nil, dismissalPolicy: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|