mycode/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift
songyc macbook 9bf617f3e5 feat(widgets): 위젯 테마 옵션 추가 — 앱 테마 일치/라이트·다크 고정/리퀴드 글라스 (CLAUDE.md §8.1)
- 홈 위젯 5종(행동 실행/목표 진행률/다짐 진행률/다짐 현황/통계) 설정에
  '위젯 테마' 옵션 추가
- 앱 테마와 일치: 앱의 라이트/다크 설정을 따름 (테마 키를 App Group으로 이관해
  위젯이 읽을 수 있게 함)
- 라이트/다크 고정: 컬러 스킴 강제 + 해당 모드 팔레트
- 리퀴드 글라스: 반투명 머티리얼 배경 + 표면 셀도 머티리얼로 변형해
  유리 질감 위에서 가독성 유지
- DEBUG 미리보기가 테마를 재현하도록 개선 (-widgetTheme light|dark|glass,
  글라스는 그라데이션 배경 위에 렌더링)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-10 09:14:31 +09:00

218 lines
8.8 KiB
Swift

//
// WidgetPreviewScreen.swift
// Haru_Danim
//
// DEBUG : .
// `-widgetPreview YES`( ) / `-widgetPreview lock`() .
// ( )
//
#if DEBUG
import SwiftUI
import WidgetKit
struct WidgetPreviewScreen: View {
let showLock: Bool
/// -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
// /
return content()
.padding(12)
.frame(width: size.width, height: size.height)
.background {
if theme == .glass {
Rectangle().fill(.ultraThinMaterial)
} else {
AppTheme.background
}
}
.environment(\.colorScheme, theme.forcedScheme ?? .light)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
.shadow(color: .black.opacity(0.08), radius: 6, y: 2)
}
}
#endif