- 아이폰 추가 4장: 05 위젯 모아 보기(실렌더) / 06 글래스 버블 메뉴 / 07 iCloud 동기화(합성) / 08 다크 모드 - 아이패드 추가 3장: 05 주간 타임테이블(벌크 시드로 꽉 찬 지난주) / 06 위젯 모아 보기 / 07 iCloud 동기화(합성) - DEBUG 전용 촬영 도구: -widgetPreview marketing(위젯 실렌더 판), -historyDaysAgo(과거 주 표시) — 출시 빌드 무영향 - frame_screenshots.swift: 신규 문구 5종 + renderSync(아이폰·아이패드 01-main 듀오 합성, 동기화 배지) - 촬영 시각 정합: SIMCTL_CHILD_TZ로 앱 내 시각을 상태바 9:41대에 맞춤 (README에 절차 기록) - 다음 버전 제출 때 ASC 각 언어 스크린샷 란에 추가 업로드 필요 (README 체크리스트에 명시) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVeduv1eNdXjk1ay4tSgBg
555 lines
24 KiB
Swift
555 lines
24 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
// MARK: - 마케팅 스크린샷용 위젯 모아 보기 (-widgetPreview marketing)
|
||
|
||
/// App Store 스크린샷 전용: 시드된 실데이터의 위젯 렌더를 디버그 라벨 없이 모아 보여준다.
|
||
/// 문구·배경 프레임은 Marketing/tools/frame_screenshots.swift가 합성하므로 이 화면은
|
||
/// "위젯들만 놓인 깨끗한 판"이 목표다. 아이폰 = 세로 1열(중형 4개 + 잠금화면 배너),
|
||
/// 아이패드 = 중형 2×2 + 대형 2개 + 배너. 새 카탈로그 문자열 없음 (위젯 자체 문구만 렌더).
|
||
struct MarketingWidgetScreen: View {
|
||
@Environment(\.colorScheme) private var colorScheme
|
||
|
||
@State private var nowEntry: NowRunningEntry?
|
||
@State private var actionEntry: ActionRunEntry?
|
||
@State private var actionLarge: ActionRunEntry?
|
||
@State private var ringEntry: QuestRingEntry?
|
||
@State private var goalEntry: GoalProgressEntry?
|
||
@State private var statusLarge: QuestStatusEntry?
|
||
@State private var statsMedium: StatsChartEntry?
|
||
|
||
var body: some View {
|
||
Group {
|
||
if DeviceLayout.isPad {
|
||
padLayout
|
||
} else {
|
||
phoneLayout
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
// 카드(위젯 박스)가 살짝 떠 보이도록 앱 배경보다 한 톤 가라앉은 그린 틴트
|
||
.background(Color(light: Color(hex: "#EDF3EE"), dark: Color(hex: "#151A16")).ignoresSafeArea())
|
||
.onAppear(perform: load)
|
||
}
|
||
|
||
private func load() {
|
||
// -premium 런치 인자가 PremiumGate 캐시에 반영되도록 매니저를 먼저 초기화
|
||
_ = PremiumManager.shared
|
||
nowEntry = NowRunningProvider.makeEntry(NowRunningConfigIntent())
|
||
actionEntry = ActionRunProvider.makeEntry(ActionRunConfigIntent(), family: .systemMedium)
|
||
actionLarge = ActionRunProvider.makeEntry(ActionRunConfigIntent(), family: .systemLarge)
|
||
ringEntry = QuestRingProvider.makeEntry(QuestRingConfigIntent(), family: .systemMedium)
|
||
goalEntry = GoalProgressProvider.makeEntry(GoalProgressConfigIntent(), family: .systemMedium)
|
||
var statusTwo = QuestStatusConfigIntent()
|
||
statusTwo.largeStyle = .twoGoals
|
||
statusLarge = QuestStatusProvider.makeEntry(statusTwo, family: .systemLarge)
|
||
statsMedium = StatsChartProvider.makeEntry(StatsChartConfigIntent(), family: .systemMedium)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var phoneLayout: some View {
|
||
VStack(spacing: 18) {
|
||
if let nowEntry {
|
||
box(.systemMedium) { NowRunningWidgetView(previewFamily: .systemMedium, entry: nowEntry) }
|
||
}
|
||
if let actionEntry {
|
||
box(.systemMedium) { ActionRunWidgetView(previewFamily: .systemMedium, entry: actionEntry) }
|
||
}
|
||
if let ringEntry {
|
||
box(.systemMedium) { QuestRingWidgetView(previewFamily: .systemMedium, entry: ringEntry) }
|
||
}
|
||
if let goalEntry {
|
||
box(.systemMedium) { GoalProgressWidgetView(previewFamily: .systemMedium, entry: goalEntry) }
|
||
}
|
||
banner
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var padLayout: some View {
|
||
VStack(spacing: 26) {
|
||
HStack(spacing: 26) {
|
||
if let nowEntry {
|
||
box(.systemMedium) { NowRunningWidgetView(previewFamily: .systemMedium, entry: nowEntry) }
|
||
}
|
||
if let goalEntry {
|
||
box(.systemMedium) { GoalProgressWidgetView(previewFamily: .systemMedium, entry: goalEntry) }
|
||
}
|
||
}
|
||
HStack(spacing: 26) {
|
||
if let ringEntry {
|
||
box(.systemMedium) { QuestRingWidgetView(previewFamily: .systemMedium, entry: ringEntry) }
|
||
}
|
||
if let statsMedium {
|
||
box(.systemMedium) { StatsChartWidgetView(previewFamily: .systemMedium, entry: statsMedium) }
|
||
}
|
||
}
|
||
HStack(alignment: .top, spacing: 26) {
|
||
if let statusLarge {
|
||
box(.systemLarge) { QuestStatusWidgetView(previewFamily: .systemLarge, entry: statusLarge) }
|
||
}
|
||
if let actionLarge {
|
||
box(.systemLarge) { ActionRunWidgetView(previewFamily: .systemLarge, entry: actionLarge) }
|
||
}
|
||
}
|
||
banner
|
||
}
|
||
}
|
||
|
||
/// 잠금화면 실시간 현황 배너 (시드의 진행 세션과 같은 독서 25분 — 기존 카탈로그 키만 사용)
|
||
private var banner: some View {
|
||
LockScreenActivityView(state: .init(
|
||
actionName: String(localized: "독서"),
|
||
symbolName: "book.fill",
|
||
colorHex: "#4A7A9D",
|
||
startedAt: .now.addingTimeInterval(-25 * 60),
|
||
extraCount: 1,
|
||
actionID: UUID().uuidString
|
||
))
|
||
.background(AppTheme.background, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||
.frame(width: 372)
|
||
.shadow(color: .black.opacity(0.10), radius: 10, y: 4)
|
||
}
|
||
|
||
/// WidgetPreviewScreen.widgetBox와 같은 규칙의 마케팅판 (라벨 없음, 그림자 강조)
|
||
private func box(_ 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)
|
||
}
|
||
return content()
|
||
.frame(width: size.width, height: size.height)
|
||
.background(AppTheme.background)
|
||
.environment(\.colorScheme, WidgetAppearance.appScheme ?? colorScheme)
|
||
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
|
||
.shadow(color: .black.opacity(0.10), radius: 10, y: 4)
|
||
}
|
||
}
|
||
#endif
|