mycode/myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift
songyc macbook 5b526fa650 feat(compat): iOS 하한 18.0 인하 — 글라스 버블 머티리얼 폴백·사용자 심볼 안전 렌더·Release 인라이너 크래시 우회
- 배포 타깃 26.0→18.0 (앱·위젯. 워치 10.0 불변)
- RadialNavigationView: glassEffect 계열을 @available(iOS 26) 선언 격리 + 18 머티리얼 원 폴백 (배치·연출 동일)
- DiaryZoomContainer.Coordinator → 비제네릭 톱레벨 DiaryZoomCoordinator + AnyView 소거 (하한 18 Release wholemodule에서 swift-frontend SILPerformanceInliner 무한 재귀 크래시 실측·우회)
- Image(safeSymbol:)/SymbolCompat: 카탈로그의 상위 OS 전용 심볼(18 기준 3개)이 교차 기기에서 빈 아이콘이 되지 않게 사용자 심볼 렌더 48곳+워치 7곳 폴백
- -symbolAuditDump 검증 인자 추가, iOS 18.5 시뮬 QA(빌드·radial 폴백·일기 줌·progressSelfTest 49 ALL PASS)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf
2026-08-11 16:31:40 +09:00

190 lines
8.3 KiB
Swift

//
// StatusComplication.swift
// Haru_DanimWatchWidgetsExtension
//
// (CLAUDE.md §9.2-1)
// - ( ) +
// - " "
//
import SwiftUI
import WidgetKit
struct StatusEntry: TimelineEntry {
let date: Date
let locked: Bool
let runningName: String?
let runningSymbol: String?
let runningStartAt: Date?
let extraCount: Int
/// ' '
var stale: Bool = false
}
struct StatusProvider: TimelineProvider {
/// ' ' .
/// .
/// ' ' ,
/// ( )
static let runningTrustInterval: TimeInterval = 2 * 3600
static func makeEntry() -> StatusEntry {
guard let snapshot = ComplicationStore.snapshot() else {
return StatusEntry(date: .now, locked: false, runningName: nil, runningSymbol: nil,
runningStartAt: nil, extraCount: 0)
}
guard snapshot.isPremium else {
return StatusEntry(date: .now, locked: true, runningName: nil, runningSymbol: nil,
runningStartAt: nil, extraCount: 0)
}
let stale = snapshot.runningStartAt != nil
&& Date.now.timeIntervalSince(snapshot.generatedAt) > Self.runningTrustInterval
return StatusEntry(
date: .now,
locked: false,
runningName: snapshot.runningName,
runningSymbol: snapshot.runningSymbol,
runningStartAt: snapshot.runningStartAt,
extraCount: snapshot.extraRunningCount,
stale: stale
)
}
func placeholder(in context: Context) -> StatusEntry {
StatusEntry(date: .now, locked: false, runningName: String(localized: "독서"), runningSymbol: "book.fill",
runningStartAt: .now, extraCount: 0)
}
func getSnapshot(in context: Context, completion: @escaping (StatusEntry) -> Void) {
completion(Self.makeEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<StatusEntry>) -> Void) {
completion(Timeline(entries: [Self.makeEntry()], policy: .after(.now.addingTimeInterval(15 * 60))))
}
}
struct StatusComplicationView: View {
@Environment(\.widgetFamily) private var envFamily
/// DEBUG
var previewFamily: WidgetFamily? = nil
private var family: WidgetFamily { previewFamily ?? envFamily }
let entry: StatusEntry
var body: some View {
Group {
if entry.locked {
ComplicationLockedView()
} else if entry.stale {
// ' '
switch family {
case .accessoryInline:
Text("측정 표시 새로고침 필요")
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 4) {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 11))
Text("하루 다님")
.font(.headline)
}
Text("측정 표시가 오래됐어요")
.font(.caption2)
Text("앱을 열면 새로고침돼요")
.font(.caption2)
.foregroundStyle(.secondary)
}
default:
VStack(spacing: 2) {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 14, weight: .semibold))
Text("새로고침")
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
}
} else if let name = entry.runningName {
switch family {
case .accessoryInline:
if entry.extraCount > 0 {
Text("\(name) +\(entry.extraCount)")
} else {
Text(name)
}
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 4) {
Image(safeSymbol: entry.runningSymbol ?? "record.circle")
.font(.system(size: 11))
Text(name)
.font(.headline)
.lineLimit(1)
// ( §9.2-1)
if entry.extraCount > 0 {
Text("+\(entry.extraCount)")
.font(.caption2.weight(.bold))
.foregroundStyle(.secondary)
}
}
if let start = entry.runningStartAt {
Text(start, style: .timer)
.font(.system(.body, design: .rounded).monospacedDigit())
}
Text("측정 중")
.font(.caption2)
.foregroundStyle(.secondary)
}
default:
VStack(spacing: 1) {
Image(safeSymbol: entry.runningSymbol ?? "record.circle")
.font(.system(size: 14, weight: .semibold))
if let start = entry.runningStartAt {
Text(start, style: .timer)
.font(.system(size: 11).monospacedDigit())
.multilineTextAlignment(.center)
}
if entry.extraCount > 0 {
Text("+\(entry.extraCount)")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(.secondary)
}
}
}
} else {
switch family {
case .accessoryInline:
Text("실행 중 아님")
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 1) {
Text("하루 다님")
.font(.headline)
Text("실행 중 아님")
.font(.caption2)
.foregroundStyle(.secondary)
}
default:
VStack(spacing: 2) {
Image(systemName: "pause.circle")
.font(.system(size: 16))
Text("없음")
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
}
}
}
.containerBackground(.clear, for: .widget)
}
}
struct StatusComplication: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "HaruWatchStatus", provider: StatusProvider()) { entry in
StatusComplicationView(entry: entry)
}
.configurationDisplayName("현재 현황")
.description("측정 중인 행동과 경과 시간을 표시해요. (프리미엄)")
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner])
}
}