- AppGroup.defaults를 단일 인스턴스(static let)로 변경: 접근마다 새 UserDefaults를 만들어 @AppStorage(store:) 관찰이 끊기던 문제 수정 → 설정에서 테마를 바꾸면 재시작 없이 즉시 적용됨 - 위젯 containerBackground에 강제 컬러 스킴을 직접 지정: 시스템이 배경을 위젯 환경으로 그려 라이트/다크 고정·앱 테마 일치가 실제 위젯에서 무시되던 문제 수정 - 테마 변경 시 위젯 타임라인 갱신 (앱 테마와 일치 옵션 즉시 반영) - 리퀴드 글라스 실구현: 머티리얼+하이라이트 배경(GlassWidgetBackground), 유리 테두리 셀(WidgetCellBackground), 색이 비치는 틴트 셀 (WidgetTintedCellBackground, 행동 실행 위젯 적용) - 검증 인자 -themeAutoToggle YES 추가 (3초 후 테마 반전) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
150 lines
5.0 KiB
Swift
150 lines
5.0 KiB
Swift
//
|
|
// WidgetTheme.swift
|
|
// Haru_DanimWidgets
|
|
//
|
|
// 위젯 테마 옵션 (CLAUDE.md §8.1)
|
|
// 1. 앱 테마와 일치 2. 라이트/다크 고정 3. 리퀴드 글라스(유리 질감 배경)
|
|
//
|
|
|
|
import AppIntents
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
enum WidgetThemeOption: String, AppEnum {
|
|
case matchApp
|
|
case light
|
|
case dark
|
|
case glass
|
|
|
|
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "위젯 테마")
|
|
static let caseDisplayRepresentations: [WidgetThemeOption: DisplayRepresentation] = [
|
|
.matchApp: "앱 테마와 일치",
|
|
.light: "라이트 고정",
|
|
.dark: "다크 고정",
|
|
.glass: "리퀴드 글라스",
|
|
]
|
|
|
|
/// 강제할 컬러 스킴. 리퀴드 글라스는 시스템 모드를 따른다.
|
|
var forcedScheme: ColorScheme? {
|
|
switch self {
|
|
case .light:
|
|
return .light
|
|
case .dark:
|
|
return .dark
|
|
case .matchApp:
|
|
let raw = AppGroup.defaults.string(forKey: SettingsKeys.theme)
|
|
?? UserDefaults.standard.string(forKey: SettingsKeys.theme) ?? "light"
|
|
return raw == "dark" ? .dark : .light
|
|
case .glass:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 환경값: 리퀴드 글라스 여부 (셀 표면 스타일 변형용)
|
|
|
|
private struct WidgetGlassKey: EnvironmentKey {
|
|
static let defaultValue = false
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var widgetGlass: Bool {
|
|
get { self[WidgetGlassKey.self] }
|
|
set { self[WidgetGlassKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - 테마 적용 모디파이어
|
|
|
|
struct WidgetThemeModifier: ViewModifier {
|
|
let theme: WidgetThemeOption
|
|
|
|
func body(content: Content) -> some View {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 리퀴드 글라스 배경 (§8.1-3): 반투명 머티리얼 + 대각 하이라이트로 유리 질감을 표현
|
|
struct GlassWidgetBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
Rectangle().fill(.ultraThinMaterial)
|
|
LinearGradient(
|
|
colors: [.white.opacity(0.28), .white.opacity(0.06), .clear],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// 위젯 루트에 테마 옵션 적용 (컬러 스킴 강제 + 배경)
|
|
func widgetTheme(_ theme: WidgetThemeOption) -> some View {
|
|
modifier(WidgetThemeModifier(theme: theme))
|
|
}
|
|
}
|
|
|
|
/// 셀 카드 배경: 일반 테마는 표면색, 리퀴드 글라스는 머티리얼 + 유리 테두리
|
|
struct WidgetCellBackground: View {
|
|
@Environment(\.widgetGlass) private var glass
|
|
|
|
var body: some View {
|
|
if glass {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(.thinMaterial)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.strokeBorder(.white.opacity(0.25), lineWidth: 1)
|
|
}
|
|
} else {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(AppTheme.surface)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 행동 실행 셀처럼 꼬리표 색이 배경인 셀의 글라스 변형:
|
|
/// 반투명 머티리얼 위에 색을 얹어 유리 너머로 색이 비치는 느낌을 낸다
|
|
struct WidgetTintedCellBackground: View {
|
|
@Environment(\.widgetGlass) private var glass
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
if glass {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(.thinMaterial)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(color.opacity(0.5))
|
|
}
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.strokeBorder(.white.opacity(0.3), lineWidth: 1)
|
|
}
|
|
} else {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(color)
|
|
}
|
|
}
|
|
}
|