diff --git a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift index 2a4b7b9..997ff23 100644 --- a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift +++ b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift @@ -13,6 +13,7 @@ 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") @@ -209,7 +210,8 @@ struct WidgetPreviewScreen: View { AppTheme.background } } - .environment(\.colorScheme, theme.forcedScheme ?? .light) + // 글라스는 시스템 모드를 따르므로 스킴을 강제하지 않는다 + .environment(\.colorScheme, theme.forcedScheme ?? systemScheme) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) .shadow(color: .black.opacity(0.08), radius: 6, y: 2) } diff --git a/myApp/HaruDanim/Widgets/ActionRunWidget.swift b/myApp/HaruDanim/Widgets/ActionRunWidget.swift index 214d0c6..ca07a15 100644 --- a/myApp/HaruDanim/Widgets/ActionRunWidget.swift +++ b/myApp/HaruDanim/Widgets/ActionRunWidget.swift @@ -123,6 +123,7 @@ struct ActionRunCellView: View { .lineLimit(1) } .foregroundStyle(.white) + .glassTextShadow() .padding(fullBleed ? 14 : (compact ? 8 : 12)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background(WidgetTintedCellBackground(color: cell.color, fullBleed: fullBleed)) diff --git a/myApp/HaruDanim/Widgets/GoalWidgets.swift b/myApp/HaruDanim/Widgets/GoalWidgets.swift index 7f983cd..8967dda 100644 --- a/myApp/HaruDanim/Widgets/GoalWidgets.swift +++ b/myApp/HaruDanim/Widgets/GoalWidgets.swift @@ -85,6 +85,7 @@ struct GoalBarsCellView: View { SpanBarRow(label: "주간", ratio: goal.weekRatio, color: goal.color) SpanBarRow(label: "월간", ratio: goal.monthRatio, color: goal.color) } + .glassTextShadow() .padding(fullBleed ? 14 : 10) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(WidgetCellBackground(fullBleed: fullBleed)) @@ -240,6 +241,7 @@ struct QuestRingCellView: View { .lineLimit(1) .foregroundStyle(.secondary) } + .glassTextShadow() } } @@ -285,6 +287,7 @@ struct GoalQuestGridWidgetView: View { } } } + .glassTextShadow() .padding(13) .widgetTheme(entry.theme) } diff --git a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift index 43b1643..30a90a8 100644 --- a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift +++ b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift @@ -102,6 +102,7 @@ struct LockGoalWidgetView: View { default: VStack(spacing: 2) { Image(systemName: "crown.fill") + .widgetAccentable() Text("프리미엄") .font(.caption2) } @@ -132,31 +133,45 @@ struct LockGoalWidgetView: View { .containerBackground(.clear, for: .widget) } + /// 잠금화면(비브런트/악센트 렌더링)에서는 시스템이 색을 덮어쓰므로 + /// 배경은 시스템 표준(AccessoryWidgetBackground)만 쓰고, + /// 텍스트는 .primary + 강조 요소는 .widgetAccentable()로 렌더링을 맡긴다 @ViewBuilder private func circularView(_ goal: GoalSnapshot) -> some View { switch entry.style { case .gauge: Gauge(value: min(max(entry.ratio, 0), 1)) { Image(systemName: goal.symbolName) + .widgetAccentable() } currentValueLabel: { Text("\(Int((entry.ratio * 100).rounded()))") .font(.system(size: 14, weight: .bold).monospacedDigit()) + .foregroundStyle(.primary) } .gaugeStyle(.accessoryCircular) case .number: - VStack(spacing: 0) { - Text("\(Int((entry.ratio * 100).rounded()))%") - .font(.system(size: 16, weight: .bold).monospacedDigit()) - Text(entry.spanLabel) - .font(.system(size: 9)) - .foregroundStyle(.secondary) + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 0) { + Text("\(Int((entry.ratio * 100).rounded()))%") + .font(.system(size: 16, weight: .bold).monospacedDigit()) + .foregroundStyle(.primary) + .widgetAccentable() + Text(entry.spanLabel) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } } case .dots: // 다짐 달성 여부를 점으로 (최대 6개) - VStack(spacing: 3) { - Image(systemName: goal.symbolName) - .font(.system(size: 12, weight: .semibold)) - questDots(goal, size: 6) + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 3) { + Image(systemName: goal.symbolName) + .font(.system(size: 12, weight: .semibold)) + .widgetAccentable() + questDots(goal, size: 6) + } } } } @@ -170,6 +185,8 @@ struct LockGoalWidgetView: View { .font(.caption.weight(.semibold)) .lineLimit(1) } + .foregroundStyle(.primary) + .widgetAccentable() Gauge(value: min(max(entry.ratio, 0), 1)) { EmptyView() } .gaugeStyle(.accessoryLinearCapacity) HStack { diff --git a/myApp/HaruDanim/Widgets/QuestRingWidget.swift b/myApp/HaruDanim/Widgets/QuestRingWidget.swift index e5a2a69..34dc322 100644 --- a/myApp/HaruDanim/Widgets/QuestRingWidget.swift +++ b/myApp/HaruDanim/Widgets/QuestRingWidget.swift @@ -122,6 +122,7 @@ struct QuestRingActionCellView: View { .font(.caption2.monospacedDigit()) .foregroundStyle(.secondary) } + .glassTextShadow() .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(fullBleed ? 10 : 6) .background(WidgetCellBackground(fullBleed: fullBleed)) diff --git a/myApp/HaruDanim/Widgets/StatsChartWidget.swift b/myApp/HaruDanim/Widgets/StatsChartWidget.swift index c4966c1..8e997f7 100644 --- a/myApp/HaruDanim/Widgets/StatsChartWidget.swift +++ b/myApp/HaruDanim/Widgets/StatsChartWidget.swift @@ -190,6 +190,7 @@ struct StatsChartWidgetView: View { } } } + .glassTextShadow() .padding(13) .widgetTheme(entry.theme) } diff --git a/myApp/HaruDanim/Widgets/WidgetTheme.swift b/myApp/HaruDanim/Widgets/WidgetTheme.swift index 50ef3ab..3438c5e 100644 --- a/myApp/HaruDanim/Widgets/WidgetTheme.swift +++ b/myApp/HaruDanim/Widgets/WidgetTheme.swift @@ -57,28 +57,37 @@ extension EnvironmentValues { // MARK: - 테마 적용 모디파이어 struct WidgetThemeModifier: ViewModifier { + @Environment(\.widgetRenderingMode) private var renderingMode let theme: WidgetThemeOption func body(content: Content) -> some View { - switch theme { - case .glass: - // 유리 질감 배경: 반투명 머티리얼 위에서 잘 보이도록 셀도 머티리얼로 변형 + if renderingMode != .fullColor { + // 틴트/클리어(리퀴드 글라스) 홈 화면: 시스템이 배경을 자체 유리로 대체하고 + // 색을 단색조로 재해석하므로, 커스텀 배경·강제 컬러 스킴을 얹으면 + // 시스템 유리 위에서 텍스트가 묻힌다 → 시스템 기본(바이브런트) 렌더링에 맡긴다 content - .environment(\.widgetGlass, true) - .containerBackground(for: .widget) { - GlassWidgetBackground() - } - case .light, .dark, .matchApp: - let scheme = theme.forcedScheme ?? .light - content - .environment(\.colorScheme, scheme) - .containerBackground(for: .widget) { - // containerBackground는 시스템이 위젯 자체 환경(시스템 모드)으로 그리므로 - // 콘텐츠에 적용한 강제 스킴이 전파되지 않는다 → 배경에도 직접 지정해야 - // '라이트/다크 고정'과 '앱 테마와 일치'가 실제 위젯에서 적용된다 - AppTheme.background - .environment(\.colorScheme, scheme) - } + .containerBackground(for: .widget) { Color.clear } + } else { + switch theme { + case .glass: + // 유리 질감 배경: 반투명 머티리얼 위에서 잘 보이도록 셀도 머티리얼로 변형 + content + .environment(\.widgetGlass, true) + .containerBackground(for: .widget) { + GlassWidgetBackground() + } + case .light, .dark, .matchApp: + let scheme = theme.forcedScheme ?? .light + content + .environment(\.colorScheme, scheme) + .containerBackground(for: .widget) { + // containerBackground는 시스템이 위젯 자체 환경(시스템 모드)으로 그리므로 + // 콘텐츠에 적용한 강제 스킴이 전파되지 않는다 → 배경에도 직접 지정해야 + // '라이트/다크 고정'과 '앱 테마와 일치'가 실제 위젯에서 적용된다 + AppTheme.background + .environment(\.colorScheme, scheme) + } + } } } } @@ -97,11 +106,30 @@ struct GlassWidgetBackground: View { } } +/// 리퀴드 글라스 위에서 텍스트·아이콘이 배경과 분리돼 보이도록 은은한 그림자를 얹는다. +/// 글라스 테마가 아닐 때는 아무것도 하지 않으므로 셀 콘텐츠에 무조건 걸어도 된다. +struct GlassTextShadowModifier: ViewModifier { + @Environment(\.widgetGlass) private var glass + + func body(content: Content) -> some View { + if glass { + content.shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) + } else { + content + } + } +} + extension View { /// 위젯 루트에 테마 옵션 적용 (컬러 스킴 강제 + 배경) func widgetTheme(_ theme: WidgetThemeOption) -> some View { modifier(WidgetThemeModifier(theme: theme)) } + + /// 리퀴드 글라스 테마에서만 적용되는 텍스트 그림자 (시인성 보강) + func glassTextShadow() -> some View { + modifier(GlassTextShadowModifier()) + } } /// 셀 카드 배경: 일반 테마는 표면색, 리퀴드 글라스는 머티리얼 + 유리 테두리 @@ -147,9 +175,10 @@ struct WidgetTintedCellBackground: View { @ViewBuilder private func styled(_ shape: some InsettableShape) -> some View { if glass { + // 흰 텍스트가 밝은 배경화면에 씻겨 보이지 않도록 틴트를 충분히 진하게 얹는다 shape.fill(.thinMaterial) - .overlay { shape.fill(color.opacity(0.5)) } - .overlay { shape.strokeBorder(.white.opacity(0.3), lineWidth: 1) } + .overlay { shape.fill(color.opacity(0.72)) } + .overlay { shape.strokeBorder(.white.opacity(0.35), lineWidth: 1) } } else { shape.fill(color) }