fix(theme): 테마 즉시 적용 및 위젯 테마 실적용 + 리퀴드 글라스 구현
- 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
This commit is contained in:
parent
828cf2e906
commit
e5550eb106
@ -38,6 +38,14 @@ struct ContentView: View {
|
||||
DebugSeed.seedIfRequested(context: context)
|
||||
DebugSeed.autoStartIfRequested(context: context)
|
||||
DebugSeed.pinGoalsIfRequested(context: context)
|
||||
// 검증용: -themeAutoToggle YES → 3초 후 설정 화면과 같은 경로(App Group defaults)로
|
||||
// 테마를 반전시켜 '재시작 없이 즉시 적용'되는지 확인
|
||||
if UserDefaults.standard.bool(forKey: "themeAutoToggle") {
|
||||
Task {
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
AppGroup.defaults.set(theme == "dark" ? "light" : "dark", forKey: SettingsKeys.theme)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
try? await Task.sleep(for: .seconds(1.2))
|
||||
withAnimation(.easeOut(duration: 0.4)) {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import WidgetKit
|
||||
|
||||
struct SettingsView: View {
|
||||
@Query(sort: [SortDescriptor(\Goal.sortOrder), SortDescriptor(\Goal.createdAt)]) private var goals: [Goal]
|
||||
@ -142,6 +143,10 @@ struct SettingsView: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppTheme.background)
|
||||
.navigationTitle("설정")
|
||||
.onChange(of: theme) {
|
||||
// '앱 테마와 일치' 옵션의 위젯이 바뀐 테마를 바로 반영하도록 갱신
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
#if DEBUG
|
||||
// 검증용: -settingsScrollGoal YES → 모음 탭 목표 섹션, -settingsScrollPremium YES → 프리미엄 섹션까지 자동 스크롤
|
||||
.onAppear {
|
||||
|
||||
@ -204,7 +204,7 @@ struct WidgetPreviewScreen: View {
|
||||
.frame(width: size.width, height: size.height)
|
||||
.background {
|
||||
if theme == .glass {
|
||||
Rectangle().fill(.ultraThinMaterial)
|
||||
GlassWidgetBackground()
|
||||
} else {
|
||||
AppTheme.background
|
||||
}
|
||||
|
||||
@ -18,9 +18,10 @@ import Observation
|
||||
nonisolated enum AppGroup {
|
||||
static let identifier = "group.com.yechan.HaruDanim"
|
||||
|
||||
static var defaults: UserDefaults {
|
||||
UserDefaults(suiteName: identifier) ?? .standard
|
||||
}
|
||||
/// 반드시 단일 인스턴스여야 함: @AppStorage(store:)는 전달받은 인스턴스를 관찰하므로
|
||||
/// 접근할 때마다 새 UserDefaults를 만들면 다른 화면의 값 변경이 전파되지 않는다
|
||||
/// (테마를 바꿔도 재시작 전까지 적용되지 않던 버그의 원인)
|
||||
static let defaults: UserDefaults = UserDefaults(suiteName: identifier) ?? .standard
|
||||
}
|
||||
|
||||
// MARK: - 무료 한도 / 프리미엄 기능
|
||||
|
||||
@ -123,10 +123,7 @@ struct ActionRunCellView: View {
|
||||
.foregroundStyle(.white)
|
||||
.padding(compact ? 8 : 12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(cell.color)
|
||||
)
|
||||
.background(WidgetTintedCellBackground(color: cell.color))
|
||||
.overlay {
|
||||
if cell.isRunning {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
|
||||
@ -66,12 +66,33 @@ struct WidgetThemeModifier: ViewModifier {
|
||||
content
|
||||
.environment(\.widgetGlass, true)
|
||||
.containerBackground(for: .widget) {
|
||||
Rectangle().fill(.ultraThinMaterial)
|
||||
GlassWidgetBackground()
|
||||
}
|
||||
case .light, .dark, .matchApp:
|
||||
let scheme = theme.forcedScheme ?? .light
|
||||
content
|
||||
.environment(\.colorScheme, theme.forcedScheme ?? .light)
|
||||
.containerBackground(AppTheme.background, for: .widget)
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,12 +104,46 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 셀 카드 배경: 일반 테마는 표면색, 리퀴드 글라스는 머티리얼
|
||||
/// 셀 카드 배경: 일반 테마는 표면색, 리퀴드 글라스는 머티리얼 + 유리 테두리
|
||||
struct WidgetCellBackground: View {
|
||||
@Environment(\.widgetGlass) private var glass
|
||||
|
||||
var body: some View {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(glass ? AnyShapeStyle(.thinMaterial) : AnyShapeStyle(AppTheme.surface))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user