// // WidgetPreviewScreen.swift // Haru_Danim // // DEBUG 전용: 위젯 뷰들을 앱 안에서 실제 크기로 렌더링해 검증하는 화면. // 런치 인자 `-widgetPreview YES`(홈 위젯) / `-widgetPreview lock`(잠금화면)으로 진입. // (시뮬레이터 홈 화면에 위젯을 자동으로 추가할 방법이 없어 만든 검증 도구) // #if DEBUG import SwiftUI import WidgetKit struct WidgetPreviewScreen: View { let showLock: Bool /// 앱 테마 "system"일 때 미리보기 박스가 따를 현재 스킴 @Environment(\.colorScheme) private var colorScheme // ① 행동 실행 @State private var actionSmall: ActionRunEntry? @State private var actionMedium: ActionRunEntry? @State private var actionLarge4: ActionRunEntry? @State private var actionLarge8: ActionRunEntry? // ② 목표 진행률 @State private var goalSmall: GoalProgressEntry? @State private var goalMediumTwo: GoalProgressEntry? @State private var goalMediumQuests: GoalProgressEntry? @State private var goalLargeFour: GoalProgressEntry? @State private var goalLargeTwoQuests: GoalProgressEntry? @State private var goalLargeAllQuests: GoalProgressEntry? // ③ 다짐 진행률 @State private var ringSmall: QuestRingEntry? @State private var ringMedium: QuestRingEntry? @State private var ringLarge: QuestRingEntry? /// '이하 유지' 다짐이 있으면 소형 한 칸 추가 (한도 문구 + 누적값 표시 확인용) @State private var ringAtMostSmall: QuestRingEntry? // ④ 다짐 현황 @State private var statusSmall: QuestStatusEntry? @State private var statusMedium: QuestStatusEntry? @State private var statusLargeMany: QuestStatusEntry? @State private var statusLargeFourGoals: QuestStatusEntry? @State private var statusLargeTwoGoals: QuestStatusEntry? // ⑤ 통계 @State private var statsSmall: StatsChartEntry? @State private var statsMedium: StatsChartEntry? @State private var statsLarge: StatsChartEntry? // ⑥ 현재 진행 중 @State private var nowEntry: NowRunningEntry? @State private var nowMulti: NowRunningEntry? @State private var nowEmpty: NowRunningEntry? // 잠금화면 @State private var lockEntries: [(String, LockGoalEntry)] = [] var body: some View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 16) { if showLock { lockSection } else { homeSection } } .padding() } .background { Color(white: 0.9).ignoresSafeArea() } .onAppear { load() // -widgetPreviewScroll B|C|D|S → 해당 섹션으로 자동 스크롤 if let anchor = UserDefaults.standard.string(forKey: "widgetPreviewScroll") { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { proxy.scrollTo(anchor, anchor: .top) } } } } } } private func load() { // -premium 런치 인자가 PremiumGate 캐시에 반영되도록 매니저를 먼저 초기화 _ = PremiumManager.shared if showLock { var gauge = LockGoalConfigIntent() gauge.style = .gauge var number = LockGoalConfigIntent() number.style = .number var dots = LockGoalConfigIntent() dots.style = .dots // 다짐이 여러 개고 부분 진행이 섞인 목표 + 주간 기간 — 점의 '차오름' 확인용 var dotsWeek = LockGoalConfigIntent() dotsWeek.style = .dots dotsWeek.span = .week if let manyQuests = WidgetStore.defaultGoals(8).max(by: { $0.quests.count < $1.quests.count }) { dotsWeek.goal = GoalEntity(manyQuests) } lockEntries = [ ("gauge", LockGoalProvider.makeEntry(gauge)), ("number", LockGoalProvider.makeEntry(number)), ("dots", LockGoalProvider.makeEntry(dots)), ("dots·주간·다짐多", LockGoalProvider.makeEntry(dotsWeek)), ] return } // ① 행동 실행: 기본(4셀) + 대형 8셀 구성 let actionConfig = ActionRunConfigIntent() actionSmall = ActionRunProvider.makeEntry(actionConfig, family: .systemSmall) actionMedium = ActionRunProvider.makeEntry(actionConfig, family: .systemMedium) actionLarge4 = ActionRunProvider.makeEntry(actionConfig, family: .systemLarge) var actionEight = ActionRunConfigIntent() actionEight.largeStyle = .eightCells actionLarge8 = ActionRunProvider.makeEntry(actionEight, family: .systemLarge) // ② 목표 진행률: 중형 2구성 + 대형 3구성 let goalConfig = GoalProgressConfigIntent() goalSmall = GoalProgressProvider.makeEntry(goalConfig, family: .systemSmall) goalMediumTwo = GoalProgressProvider.makeEntry(goalConfig, family: .systemMedium) var goalQuests = GoalProgressConfigIntent() goalQuests.mediumStyle = .goalWithQuests goalMediumQuests = GoalProgressProvider.makeEntry(goalQuests, family: .systemMedium) goalLargeFour = GoalProgressProvider.makeEntry(goalConfig, family: .systemLarge) var goalTwoQ = GoalProgressConfigIntent() goalTwoQ.largeStyle = .twoGoalsWithQuests goalLargeTwoQuests = GoalProgressProvider.makeEntry(goalTwoQ, family: .systemLarge) var goalAllQ = GoalProgressConfigIntent() goalAllQ.largeStyle = .oneGoalAllQuests goalLargeAllQuests = GoalProgressProvider.makeEntry(goalAllQ, family: .systemLarge) // ③ 다짐 진행률 let ringConfig = QuestRingConfigIntent() ringSmall = QuestRingProvider.makeEntry(ringConfig, family: .systemSmall) ringMedium = QuestRingProvider.makeEntry(ringConfig, family: .systemMedium) ringLarge = QuestRingProvider.makeEntry(ringConfig, family: .systemLarge) if let atMost = IntentStore.quests().first(where: { $0.direction == .atMost }) { var atMostConfig = QuestRingConfigIntent() atMostConfig.quests = [QuestEntity(atMost)] ringAtMostSmall = QuestRingProvider.makeEntry(atMostConfig, family: .systemSmall) } // ④ 다짐 현황: 대형 3구성 let statusConfig = QuestStatusConfigIntent() statusSmall = QuestStatusProvider.makeEntry(statusConfig, family: .systemSmall) statusMedium = QuestStatusProvider.makeEntry(statusConfig, family: .systemMedium) statusLargeMany = QuestStatusProvider.makeEntry(statusConfig, family: .systemLarge) var statusFour = QuestStatusConfigIntent() statusFour.largeStyle = .fourGoals statusLargeFourGoals = QuestStatusProvider.makeEntry(statusFour, family: .systemLarge) var statusTwo = QuestStatusConfigIntent() statusTwo.largeStyle = .twoGoals statusLargeTwoGoals = QuestStatusProvider.makeEntry(statusTwo, family: .systemLarge) // ⑤ 통계 let statsConfig = StatsChartConfigIntent() statsSmall = StatsChartProvider.makeEntry(statsConfig, family: .systemSmall) statsMedium = StatsChartProvider.makeEntry(statsConfig, family: .systemMedium) statsLarge = StatsChartProvider.makeEntry(statsConfig, family: .systemLarge) // ⑥ 현재 진행 중: 실데이터(시드 데모의 진행 중 세션) + 여러 개 나열/빈 상태는 강제 구성 nowEntry = NowRunningProvider.makeEntry(NowRunningConfigIntent()) nowMulti = .sample() nowEmpty = NowRunningEntry(date: .now, locked: false, theme: .matchApp, primary: nil, others: [], todayTickingBase: nil) } // MARK: 홈 위젯 @ViewBuilder private var homeSection: some View { liveActivityRow actionRows goalRows ringRows statusRows statsRows nowRunningRows } /// 잠금화면 라이브 액티비티 배너 (종료 버튼 포함) — 실기기 잠금화면 렌더는 자동화가 /// 안 되므로 배너 뷰를 직접 그려 레이아웃·버튼 표시를 확인한다 (탭 동작은 실기기에서) @ViewBuilder private var liveActivityRow: some View { row("잠금화면 실시간 현황 (측정 종료 버튼)") { LockScreenActivityView(state: .init( actionName: String(localized: "독서"), symbolName: "book.fill", colorHex: "#4A7A9D", startedAt: .now.addingTimeInterval(-25 * 60), extraCount: 1, actionID: UUID().uuidString )) .background(.background, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) .frame(width: 372) } } @ViewBuilder private var actionRows: some View { if let small = actionSmall, let medium = actionMedium { row("① 행동 실행 · 소형/중형(2×2)") { widgetBox(.systemSmall) { ActionRunWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { ActionRunWidgetView(previewFamily: .systemMedium, entry: medium) } } } if let large = actionLarge4 { row("① 행동 실행 · 대형(큰 셀 4개)") { widgetBox(.systemLarge) { ActionRunWidgetView(previewFamily: .systemLarge, entry: large) } } .id("A4") } if let large = actionLarge8 { row("① 행동 실행 · 대형(작은 셀 8개)") { widgetBox(.systemLarge) { ActionRunWidgetView(previewFamily: .systemLarge, entry: large) } } .id("A8") } } @ViewBuilder private var goalRows: some View { if let small = goalSmall, let medium = goalMediumTwo { row("② 목표 진행률 · 소형/중형(목표 2개)") { widgetBox(.systemSmall) { GoalProgressWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { GoalProgressWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("B") } if let medium = goalMediumQuests { row("② 목표 진행률 · 중형(목표+다짐)") { widgetBox(.systemMedium) { GoalProgressWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("B2") } if let large = goalLargeFour { row("② 목표 진행률 · 대형(목표 4개)") { widgetBox(.systemLarge) { GoalProgressWidgetView(previewFamily: .systemLarge, entry: large) } } .id("B4") } if let large = goalLargeTwoQuests { row("② 목표 진행률 · 대형(목표+다짐 2개)") { widgetBox(.systemLarge) { GoalProgressWidgetView(previewFamily: .systemLarge, entry: large) } } .id("B2Q") } if let large = goalLargeAllQuests { row("② 목표 진행률 · 대형(모든 다짐)") { widgetBox(.systemLarge) { GoalProgressWidgetView(previewFamily: .systemLarge, entry: large) } } .id("BALL") } } @ViewBuilder private var ringRows: some View { if let small = ringSmall, let medium = ringMedium { row("③ 다짐 진행률 · 소형/중형") { widgetBox(.systemSmall) { QuestRingWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { QuestRingWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("C") } if let atMostSmall = ringAtMostSmall { row("③ 다짐 진행률 · 이하 유지(소형)") { widgetBox(.systemSmall) { QuestRingWidgetView(previewFamily: .systemSmall, entry: atMostSmall) } } .id("CM") } if let large = ringLarge { row("③ 다짐 진행률 · 대형(2×2)") { widgetBox(.systemLarge) { QuestRingWidgetView(previewFamily: .systemLarge, entry: large) } } .id("CL") } } @ViewBuilder private var statusRows: some View { if let small = statusSmall, let medium = statusMedium { row("④ 다짐 현황 · 소형(2×2)/중형(4×2)") { widgetBox(.systemSmall) { QuestStatusWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { QuestStatusWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("D") } if let large = statusLargeMany { row("④ 다짐 현황 · 대형(다짐 16개)") { widgetBox(.systemLarge) { QuestStatusWidgetView(previewFamily: .systemLarge, entry: large) } } .id("D16") } if let large = statusLargeFourGoals { row("④ 다짐 현황 · 대형(목표 4개)") { widgetBox(.systemLarge) { QuestStatusWidgetView(previewFamily: .systemLarge, entry: large) } } .id("D4G") } if let large = statusLargeTwoGoals { row("④ 다짐 현황 · 대형(목표 2개)") { widgetBox(.systemLarge) { QuestStatusWidgetView(previewFamily: .systemLarge, entry: large) } } .id("D2G") } } @ViewBuilder private var statsRows: some View { if let small = statsSmall, let medium = statsMedium { row("⑤ 통계 · 소형/중형") { widgetBox(.systemSmall) { StatsChartWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { StatsChartWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("S") } if let large = statsLarge { row("⑤ 통계 · 대형") { widgetBox(.systemLarge) { StatsChartWidgetView(previewFamily: .systemLarge, entry: large) } } .id("SL") } } @ViewBuilder private var nowRunningRows: some View { if let entry = nowEntry { row("⑥ 현재 진행 중 · 소형/중형") { widgetBox(.systemSmall) { NowRunningWidgetView(previewFamily: .systemSmall, entry: entry) } widgetBox(.systemMedium) { NowRunningWidgetView(previewFamily: .systemMedium, entry: entry) } } .id("R") row("⑥ 현재 진행 중 · 대형") { widgetBox(.systemLarge) { NowRunningWidgetView(previewFamily: .systemLarge, entry: entry) } } .id("RL") } if let multi = nowMulti { row("⑥ 현재 진행 중 · 대형(여러 개, 샘플)") { widgetBox(.systemLarge) { NowRunningWidgetView(previewFamily: .systemLarge, entry: multi) } } .id("RM") } if let empty = nowEmpty { row("⑥ 현재 진행 중 · 빈 상태(소형/중형)") { widgetBox(.systemSmall) { NowRunningWidgetView(previewFamily: .systemSmall, entry: empty) } widgetBox(.systemMedium) { NowRunningWidgetView(previewFamily: .systemMedium, entry: empty) } } .id("RE") } } // MARK: 잠금화면 위젯 @ViewBuilder private var lockSection: some View { ForEach(lockEntries, id: \.0) { name, entry in row("잠금화면 · \(name)") { ZStack { Color.black LockGoalWidgetView(previewFamily: .accessoryCircular, entry: entry) .foregroundStyle(.white) } .frame(width: 72, height: 72) .clipShape(Circle()) ZStack { Color.black LockGoalWidgetView(previewFamily: .accessoryRectangular, entry: entry) .foregroundStyle(.white) .padding(6) } .frame(width: 172, height: 76) .clipShape(RoundedRectangle(cornerRadius: 16)) } } if let entry = lockEntries.first?.1 { row("잠금화면 · inline") { ZStack { Color.black LockGoalWidgetView(previewFamily: .accessoryInline, entry: entry) .foregroundStyle(.white) } .frame(width: 230, height: 32) .clipShape(Capsule()) } } } // MARK: 레이아웃 헬퍼 private func row(_ title: String, @ViewBuilder content: () -> some View) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 12, content: content) } } /// -widgetRenderMode accented|vibrant → 색조/투명 홈 화면 렌더링 모드를 강제해 /// white-on-white 대비 문제를 앱 안에서 검증 (기본은 실제와 동일한 fullColor) private var forcedRenderingMode: WidgetRenderingMode { switch UserDefaults.standard.string(forKey: "widgetRenderMode") { case "accented": return .accented case "vibrant": return .vibrant default: return .fullColor } } private func widgetBox(_ family: WidgetFamily, @ViewBuilder content: () -> some View) -> some View { let size: CGSize = switch family { case .systemMedium: CGSize(width: 338, height: 158) case .systemLarge: CGSize(width: 338, height: 354) default: CGSize(width: 158, height: 158) } // containerBackground는 실제 위젯 컨텍스트 밖에서는 무시되므로 // 미리보기 박스가 앱 테마 배경/컬러 스킴을 직접 재현한다 // contentMarginsDisabled 적용 후에는 위젯 뷰가 여백을 스스로 관리한다 return content() .frame(width: size.width, height: size.height) .background(AppTheme.background) // 앱 테마가 "system"(nil)이면 강제하지 않는다 — 실제 위젯의 시스템 따르기와 동일 .environment(\.colorScheme, WidgetAppearance.appScheme ?? colorScheme) .environment(\.widgetRenderingMode, forcedRenderingMode) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) .shadow(color: .black.opacity(0.08), radius: 6, y: 2) } } #endif