// // WidgetPreviewScreen.swift // Haru_Danim // // DEBUG 전용: 위젯 뷰들을 앱 안에서 실제 크기로 렌더링해 검증하는 화면. // 런치 인자 `-widgetPreview YES`(홈 위젯) / `-widgetPreview lock`(잠금화면)으로 진입. // (시뮬레이터 홈 화면에 위젯을 자동으로 추가할 방법이 없어 만든 검증 도구) // #if DEBUG import SwiftUI import WidgetKit struct WidgetPreviewScreen: View { let showLock: Bool @Environment(\.colorScheme) private var systemScheme /// -widgetTheme light|dark|glass (§8.1 검증용) private let theme = UserDefaults.standard.string(forKey: "widgetTheme") .flatMap(WidgetThemeOption.init(rawValue:)) ?? .matchApp @State private var actionEntry: ActionRunEntry? @State private var goalBarsEntry: GoalBarsEntry? @State private var questRingEntry: QuestRingEntry? @State private var gridEntry: GoalQuestGridEntry? @State private var statsEntrySmall: StatsChartEntry? @State private var statsEntryMedium: StatsChartEntry? @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 { if theme == .glass { // 리퀴드 글라스 확인용: 유리 질감이 드러나도록 화려한 배경 위에 렌더링 LinearGradient( colors: [.blue, .purple, .orange, .pink], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() } else { 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 lockEntries = [ ("gauge", LockGoalProvider.makeEntry(gauge)), ("number", LockGoalProvider.makeEntry(number)), ("dots", LockGoalProvider.makeEntry(dots)), ] } else { // 모든 위젯 설정에 테마 주입 (§8.1 검증) let actionConfig = ActionRunConfigIntent() actionConfig.theme = theme actionEntry = ActionRunProvider.makeEntry(actionConfig) let goalConfig = GoalBarsConfigIntent() goalConfig.theme = theme goalBarsEntry = GoalBarsProvider.makeEntry(goalConfig) let questConfig = QuestRingConfigIntent() questConfig.theme = theme questRingEntry = QuestRingProvider.makeEntry(questConfig) let gridConfig = GoalQuestGridConfigIntent() gridConfig.theme = theme gridEntry = GoalQuestGridProvider.makeEntry(gridConfig) let statsConfig = StatsChartConfigIntent() statsConfig.theme = theme statsEntrySmall = StatsChartProvider.makeEntry(statsConfig, family: .systemSmall) statsEntryMedium = StatsChartProvider.makeEntry(statsConfig, family: .systemMedium) } } // MARK: 홈 위젯 @ViewBuilder private var homeSection: some View { if let entry = actionEntry { row("A 행동 실행 · 소형/중형") { widgetBox(.systemSmall) { ActionRunWidgetView(previewFamily: .systemSmall, entry: entry) } widgetBox(.systemMedium) { ActionRunWidgetView(previewFamily: .systemMedium, entry: entry) } } row("A 행동 실행 · 대형") { widgetBox(.systemLarge) { ActionRunWidgetView(previewFamily: .systemLarge, entry: entry) } } } if let entry = goalBarsEntry { row("B 목표 진행률 · 소형/중형") { widgetBox(.systemSmall) { GoalBarsWidgetView(previewFamily: .systemSmall, entry: entry) } widgetBox(.systemMedium) { GoalBarsWidgetView(previewFamily: .systemMedium, entry: entry) } } .id("B") } if let entry = questRingEntry { row("C 다짐 진행률 · 소형/중형") { widgetBox(.systemSmall) { QuestRingWidgetView(previewFamily: .systemSmall, entry: entry) } widgetBox(.systemMedium) { QuestRingWidgetView(previewFamily: .systemMedium, entry: entry) } } .id("C") } if let entry = gridEntry { row("D 다짐 현황 · 소형/중형") { widgetBox(.systemSmall) { GoalQuestGridWidgetView(previewFamily: .systemSmall, entry: entry) } widgetBox(.systemMedium) { GoalQuestGridWidgetView(previewFamily: .systemMedium, entry: entry) } } .id("D") row("D 다짐 현황 · 대형(목표 2개)") { widgetBox(.systemLarge) { GoalQuestGridWidgetView(previewFamily: .systemLarge, entry: entry) } } } if let small = statsEntrySmall, let medium = statsEntryMedium { row("통계 · 소형/중형") { widgetBox(.systemSmall) { StatsChartWidgetView(previewFamily: .systemSmall, entry: small) } widgetBox(.systemMedium) { StatsChartWidgetView(previewFamily: .systemMedium, entry: medium) } } .id("S") } } // 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) } } 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 { if theme == .glass { GlassWidgetBackground() } else { AppTheme.background } } // 글라스는 시스템 모드를 따르므로 스킴을 강제하지 않는다 .environment(\.colorScheme, theme.forcedScheme ?? systemScheme) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) .shadow(color: .black.opacity(0.08), radius: 6, y: 2) } } #endif