diff --git a/myApp/HaruDanim/IOS/ContentView.swift b/myApp/HaruDanim/IOS/ContentView.swift index 2fecf95..a495334 100644 --- a/myApp/HaruDanim/IOS/ContentView.swift +++ b/myApp/HaruDanim/IOS/ContentView.swift @@ -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)) { diff --git a/myApp/HaruDanim/IOS/Views/SettingsView.swift b/myApp/HaruDanim/IOS/Views/SettingsView.swift index e399b5e..4a27df7 100644 --- a/myApp/HaruDanim/IOS/Views/SettingsView.swift +++ b/myApp/HaruDanim/IOS/Views/SettingsView.swift @@ -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 { diff --git a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift index e398d51..e98cd91 100644 --- a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift +++ b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift @@ -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 } diff --git a/myApp/HaruDanim/Shared/Premium.swift b/myApp/HaruDanim/Shared/Premium.swift index a1ab9de..1ad963a 100644 --- a/myApp/HaruDanim/Shared/Premium.swift +++ b/myApp/HaruDanim/Shared/Premium.swift @@ -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: - 무료 한도 / 프리미엄 기능 diff --git a/myApp/HaruDanim/Widgets/ActionRunWidget.swift b/myApp/HaruDanim/Widgets/ActionRunWidget.swift index 5170708..c6a151e 100644 --- a/myApp/HaruDanim/Widgets/ActionRunWidget.swift +++ b/myApp/HaruDanim/Widgets/ActionRunWidget.swift @@ -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) diff --git a/myApp/HaruDanim/Widgets/WidgetTheme.swift b/myApp/HaruDanim/Widgets/WidgetTheme.swift index f7aed4f..7f475be 100644 --- a/myApp/HaruDanim/Widgets/WidgetTheme.swift +++ b/myApp/HaruDanim/Widgets/WidgetTheme.swift @@ -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) + } } }