fix(widgets): 리퀴드 글라스 테마 시인성 개선 및 틴트/클리어 렌더링 모드 대응

- 글라스 테마: 틴트 셀 불투명도 0.5→0.72, 테두리 강화로 흰 텍스트 대비 확보
- 글라스 테마 전용 텍스트 그림자(glassTextShadow) 추가 — 홈 위젯 5종 셀 콘텐츠에 적용
- widgetRenderingMode != .fullColor(홈 화면 틴트/클리어)일 때 커스텀 배경·강제
  컬러 스킴을 끄고 시스템 바이브런트 렌더링에 맡기도록 분기
- 잠금화면 위젯: AccessoryWidgetBackground + widgetAccentable + .primary 전경색으로
  비브런트/악센트 렌더링에서 시인성 확보
- 미리보기 하네스: 글라스 테마가 시스템 컬러 스킴을 따르도록 수정

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-10 21:18:43 +09:00
parent bef503465c
commit bc6cd010b7
7 changed files with 85 additions and 31 deletions

View File

@ -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)
}

View File

@ -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))

View File

@ -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)
}

View File

@ -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 {

View File

@ -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))

View File

@ -190,6 +190,7 @@ struct StatsChartWidgetView: View {
}
}
}
.glassTextShadow()
.padding(13)
.widgetTheme(entry.theme)
}

View File

@ -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)
}