Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
141 lines
5.1 KiB
Swift
141 lines
5.1 KiB
Swift
//
|
|
// 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: "다크 모드 고정",
|
|
]
|
|
|
|
/// 이 옵션이 위젯에 적용할 컬러 스킴
|
|
var scheme: ColorScheme {
|
|
switch self {
|
|
case .matchApp: return WidgetAppearance.appScheme
|
|
case .light: return .light
|
|
case .dark: return .dark
|
|
}
|
|
}
|
|
}
|
|
|
|
enum WidgetAppearance {
|
|
/// 앱 설정의 테마 ("light" | "dark") → 위젯에 그대로 적용할 컬러 스킴
|
|
static var appScheme: ColorScheme {
|
|
let raw = AppGroup.defaults.string(forKey: SettingsKeys.theme)
|
|
?? UserDefaults.standard.string(forKey: SettingsKeys.theme) ?? "light"
|
|
return raw == "dark" ? .dark : .light
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
let scheme = theme.scheme
|
|
content
|
|
.environment(\.colorScheme, scheme)
|
|
.containerBackground(for: .widget) {
|
|
// containerBackground는 시스템이 위젯 자체 환경(시스템 모드)으로 그리므로
|
|
// 콘텐츠에 적용한 스킴을 배경에도 직접 지정해야 선택한 테마가 실제로 적용된다
|
|
AppTheme.background
|
|
.environment(\.colorScheme, scheme)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|