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:
parent
bef503465c
commit
bc6cd010b7
@ -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)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -190,6 +190,7 @@ struct StatsChartWidgetView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.glassTextShadow()
|
||||
.padding(13)
|
||||
.widgetTheme(entry.theme)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user