mycode/myApp/HaruDanim/IOS/Haru_DanimApp.swift
songyc macbook ec62dc83de feat(siri): background Live Activity, spoken action names, localized phrases
시리 단축어 점검에서 나온 추천 수정 일괄 적용:

- StartTimeActionIntent·StopTimeActionIntent가 LiveActivityIntent 채택 —
  시리/단축어의 백그라운드 실행에서도 Live Activity 시작이 허용되어
  앱을 열지 않아도 다이나믹 아일랜드에 측정이 표시된다.
  RunActionIntent(위젯 버튼)는 확장 프로세스 응답성 튜닝을 유지하기 위해
  의도적으로 제외.
- 행동 이름 파라미터 문구 추가: "시리야, 하루 다님에서 독서 측정 시작"처럼
  되묻지 않고 한 번에 실행. 어휘 갱신을 위해 앱 시작 시
  updateAppShortcutParameters() 호출 (새 행동은 다음 실행부터 인식).
- IOS/AppShortcuts.xcstrings 신설 — 시리 문구 ko/en/ja 지역화.
  빌드 로그의 --no-app-shortcuts-localization가 사라지고 root.ssu.yaml에
  en/ja 발화가 포함되는 것 확인.
- RunActionIntent isDiscoverable=false — 생 UUID 파라미터라 사용자가 조합할
  수 없는 위젯 전용 동작을 단축어 앱·스포트라이트에서 숨김 (위젯 버튼은
  그대로 동작).
- 결제 화면 기능 목록에 '시리 단축어' 추가. 이 과정에서 featureRow가
  String 파라미터라 기능 목록 6줄 전체가 미지역화였던 버그 발견 —
  LocalizedStringKey로 바꾸고 en/ja 12개 문자열 번역.

검증: -intentSmokeTest 9개 시나리오 전부 통과(LiveActivityIntent 전환 후
재확인), 프리미엄 화면 렌더링 확인, Debug·Store 스킴 빌드 성공.
남은 확인: 시리 음성 인식·백그라운드 Live Activity 실표시는 실기기 필요.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-13 21:08:48 +09:00

78 lines
3.5 KiB
Swift

//
// Haru_DanimApp.swift
// Haru_Danim
//
// Created by on 7/7/26.
//
import AppIntents
import SwiftUI
import SwiftData
@main
struct Haru_DanimApp: App {
/// App Group DB (·App Intents , CloudKit )
private let container: ModelContainer
@Environment(\.scenePhase) private var scenePhase
init() {
// ( shared )
PremiumManager.bootstrap(provider: StoreKitEntitlementProvider())
SettingsMigration.runIfNeeded()
container = DataStore.shared
DataStore.ensureUniqueEntityIDs(context: container.mainContext)
// · UI : (@Model) 1
LocalPrefs.adoptLegacyModelValuesIfNeeded(context: container.mainContext)
// / Live Activity·
IntentHooks.afterMutation = { context in
LiveActivityManager.sync(context: context)
WatchSyncManager.shared.pushSnapshot()
}
WatchSyncManager.shared.activate()
// CloudKit ( · ) ··
CloudSyncMonitor.shared.start(container: container)
// (\(\.$action) ) /
HaruDanimShortcuts.updateAppShortcutParameters()
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
.onChange(of: scenePhase) { _, newPhase in
// (/ )
WatchSyncManager.shared.pushSnapshot()
if newPhase == .active {
//
// ·· ' '
Self.evaluateEndedGoals(context: container.mainContext)
// ( ) Live Activity +
//
LiveActivityManager.sync(context: container.mainContext)
}
}
}
/// .
/// (GoalListView.onAppear) / .
/// evaluateIfEnded ' ' .
@MainActor
private static func evaluateEndedGoals(context: ModelContext) {
let inProgress = GoalStatus.inProgress.rawValue
let goals = (try? context.fetch(FetchDescriptor<Goal>(
predicate: #Predicate { $0.statusRaw == inProgress }
))) ?? []
guard !goals.isEmpty else { return }
let math = DayMath()
var changed = false
for goal in goals {
let before = goal.status
goal.evaluateIfEnded(math: math)
if goal.status != before { changed = true }
}
if changed { DataChange.commit(context: context) }
}
}