// // WidgetTheme.swift // Haru_DanimWidgets // // 위젯 외형 (CLAUDE.md §8.1) // - 모든 홈 위젯 설정(Intent)에 [앱 설정과 일치 / 라이트 고정 / 다크 고정] 테마 옵션 제공. // - containerBackground(iOS 17+)로 배경을 그리고, 홈 화면이 틴트/클리어(리퀴드 글라스) // 모드일 때는 시스템이 배경을 자체 유리로 대체하고 색을 재해석하므로 커스텀 배경·강제 // 스킴을 얹지 않고(Color.clear) 시스템 바이브런트 렌더링에 맡긴다 — 흰 글씨가 // 흰 배경에 묻히는(white-on-white) 버그 방어의 핵심. // import SwiftUI import WidgetKit import AppIntents // MARK: - 테마 옵션 (모든 홈 위젯 설정 공용) enum WidgetThemeOption: String, AppEnum { case matchApp case light case dark static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "테마") static let caseDisplayRepresentations: [WidgetThemeOption: DisplayRepresentation] = [ .matchApp: "앱 설정과 일치", .light: "라이트 모드 고정", .dark: "다크 모드 고정", ] /// 이 옵션이 위젯에 적용할 컬러 스킴 (nil = 강제하지 않고 시스템 모드를 따름) var scheme: ColorScheme? { switch self { case .matchApp: return WidgetAppearance.appScheme case .light: return .light case .dark: return .dark } } } enum WidgetAppearance { /// 앱 설정의 테마 ("system" | "light" | "dark") → 위젯에 적용할 컬러 스킴. /// "system"(기본)은 nil — 위젯 환경의 시스템 모드를 그대로 따른다 static var appScheme: ColorScheme? { let raw = AppGroup.defaults.string(forKey: SettingsKeys.theme) ?? UserDefaults.standard.string(forKey: SettingsKeys.theme) switch raw { case "dark": return .dark case "light": return .light default: return nil } } } // MARK: - 테마 적용 모디파이어 struct WidgetThemeModifier: ViewModifier { @Environment(\.widgetRenderingMode) private var renderingMode let theme: WidgetThemeOption func body(content: Content) -> some View { if renderingMode != .fullColor { // 틴트/클리어(리퀴드 글라스) 홈 화면: 커스텀 배경·강제 스킴을 얹으면 // 시스템 유리 위에서 텍스트가 묻힌다 → 시스템 기본(바이브런트) 렌더링에 맡긴다 content .containerBackground(for: .widget) { Color.clear } } else if let scheme = theme.scheme { content .environment(\.colorScheme, scheme) .containerBackground(for: .widget) { // containerBackground는 시스템이 위젯 자체 환경(시스템 모드)으로 그리므로 // 콘텐츠에 적용한 스킴을 배경에도 직접 지정해야 선택한 테마가 실제로 적용된다 AppTheme.background .environment(\.colorScheme, scheme) } } else { // 시스템 따르기(테마 "system"): 스킴을 강제하지 않는다 — // 동적 색(AppTheme)이 위젯 환경의 시스템 모드에 맞춰 알아서 전환된다 content .containerBackground(for: .widget) { AppTheme.background } } } } extension View { /// 위젯 루트에 테마 적용 (컬러 스킴 + containerBackground 배경) func widgetAppTheme(_ theme: WidgetThemeOption = .matchApp) -> some View { modifier(WidgetThemeModifier(theme: theme)) } } // MARK: - 셀 배경 /// 셀 카드 배경 (표면색). /// fullBleed = 셀 하나가 위젯 전체를 차지할 때 위젯 모서리(ContainerRelativeShape)에 맞춤 /// 틴트/클리어(리퀴드 글라스) 모드에서는 불투명 표면색이 흰 판으로 재해석돼 바이브런트 /// 콘텐츠와 겹치면 white-on-white가 되므로, 옅은 반투명 패널로 바꿔 시스템 유리 질감을 /// 살리면서 대비를 유지한다 (①의 틴트 셀 0.2 반투명 처리와 같은 원리). struct WidgetCellBackground: View { @Environment(\.widgetRenderingMode) private var renderingMode var fullBleed = false private var fill: AnyShapeStyle { renderingMode == .fullColor ? AnyShapeStyle(AppTheme.surface) : AnyShapeStyle(Color.white.opacity(0.14)) } var body: some View { if fullBleed { ContainerRelativeShape().fill(fill) } else { RoundedRectangle(cornerRadius: 14, style: .continuous).fill(fill) } } } /// 행동 실행 셀처럼 꼬리표 색이 배경인 셀 struct WidgetTintedCellBackground: View { let color: Color var fullBleed = false var body: some View { if fullBleed { ContainerRelativeShape().fill(color) } else { RoundedRectangle(cornerRadius: 14, style: .continuous).fill(color) } } } /// 아직 행동/다짐을 선택하지 않은 슬롯의 빈 칸 (점선 테두리) struct WidgetBlankCellView: View { var fullBleed = false var body: some View { Group { if fullBleed { ContainerRelativeShape() .strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [5, 4])) } else { RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(style: StrokeStyle(lineWidth: 1.5, dash: [5, 4])) } } .foregroundStyle(.quaternary) .frame(maxWidth: .infinity, maxHeight: .infinity) } }