From 2a77ad3d8faf1bab62b9d19ac67c334457ae5de9 Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sat, 13 Sep 2025 23:31:22 +0800 Subject: [PATCH 1/7] fix: resolve theme switching not working in appearance settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed onChange to onReceive with proper publisher for reactive theme updates - Added immediate window interface style updates when theme is changed - Wrapped UIKit updates in DispatchQueue.main.async for thread safety - Force window tint color refresh for navigation bar appearance - Added direct updateAppearance call in settings for instant feedback 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- V2er/General/V2erApp.swift | 99 +++++++++++-------- .../View/Settings/AppearanceSettingView.swift | 25 ++++- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index f35d97b..767b6e4 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -7,6 +7,7 @@ // import SwiftUI +import Combine @main struct V2erApp: App { @@ -36,7 +37,7 @@ struct V2erApp: App { updateNavigationBarAppearance(for: store.appState.settingState.appearance) updateWindowInterfaceStyle(for: store.appState.settingState.appearance) } - .onChange(of: store.appState.settingState.appearance) { newValue in + .onReceive(store.$appState.map(\.settingState.appearance)) { newValue in updateNavigationBarAppearance(for: newValue) updateWindowInterfaceStyle(for: newValue) } @@ -45,49 +46,67 @@ struct V2erApp: App { } private func updateNavigationBarAppearance(for appearance: AppearanceMode) { - let navbarAppearance = UINavigationBarAppearance() - - // Determine if we should use dark mode - let isDarkMode: Bool - switch appearance { - case .light: - isDarkMode = false - case .dark: - isDarkMode = true - case .system: - isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark + DispatchQueue.main.async { + let navbarAppearance = UINavigationBarAppearance() + + // Determine if we should use dark mode + let isDarkMode: Bool + switch appearance { + case .light: + isDarkMode = false + case .dark: + isDarkMode = true + case .system: + isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark + } + + let tintColor = isDarkMode ? UIColor.white : UIColor.black + navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor] + navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor] + navbarAppearance.backgroundColor = .clear + + let navAppearance = UINavigationBar.appearance() + navAppearance.standardAppearance = navbarAppearance + navAppearance.compactAppearance = navbarAppearance + navAppearance.scrollEdgeAppearance = navbarAppearance + navAppearance.backgroundColor = .clear + navAppearance.tintColor = tintColor + + // Force refresh of current navigation controllers + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + windowScene.windows.forEach { window in + window.subviews.forEach { _ in + window.tintColor = tintColor + } + } + } } - - let tintColor = isDarkMode ? UIColor.white : UIColor.black - navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor] - navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor] - navbarAppearance.backgroundColor = .clear - - let navAppearance = UINavigationBar.appearance() - navAppearance.standardAppearance = navbarAppearance - navAppearance.compactAppearance = navbarAppearance - navAppearance.scrollEdgeAppearance = navbarAppearance - navAppearance.backgroundColor = .clear - navAppearance.tintColor = tintColor } private func updateWindowInterfaceStyle(for appearance: AppearanceMode) { - // Get all windows and update their interface style - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - - let style: UIUserInterfaceStyle - switch appearance { - case .light: - style = .light - case .dark: - style = .dark - case .system: - style = .unspecified - } - - // Update all windows in the scene - windowScene.windows.forEach { window in - window.overrideUserInterfaceStyle = style + DispatchQueue.main.async { + // Get all windows and update their interface style + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } + + let style: UIUserInterfaceStyle + switch appearance { + case .light: + style = .light + case .dark: + style = .dark + case .system: + style = .unspecified + } + + // Update all windows in the scene + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = style + } + + // Also update the stored window if available + if let window = V2erApp.window { + window.overrideUserInterfaceStyle = style + } } } diff --git a/V2er/View/Settings/AppearanceSettingView.swift b/V2er/View/Settings/AppearanceSettingView.swift index a531739..606d9aa 100644 --- a/V2er/View/Settings/AppearanceSettingView.swift +++ b/V2er/View/Settings/AppearanceSettingView.swift @@ -11,7 +11,7 @@ import SwiftUI struct AppearanceSettingView: View { @EnvironmentObject private var store: Store @State private var selectedAppearance: AppearanceMode = .system - + var body: some View { formView .navBar("外观设置") @@ -20,6 +20,27 @@ struct AppearanceSettingView: View { } } + private func updateAppearance(_ mode: AppearanceMode) { + // Force immediate window interface style update + DispatchQueue.main.async { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } + + let style: UIUserInterfaceStyle + switch mode { + case .light: + style = .light + case .dark: + style = .dark + case .system: + style = .unspecified + } + + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = style + } + } + } + @ViewBuilder private var formView: some View { ScrollView { @@ -37,6 +58,8 @@ struct AppearanceSettingView: View { Button(action: { selectedAppearance = mode dispatch(SettingActions.ChangeAppearanceAction(appearance: mode)) + // Force immediate UI update + updateAppearance(mode) }) { HStack { Text(mode.displayName) From 33ea3559246b73c2d7207628262178483b8ae7d4 Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sat, 13 Sep 2025 23:37:26 +0800 Subject: [PATCH 2/7] fix: complete overhaul of theme switching mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added notification-based immediate UI updates for theme changes - Modified WindowGroup to properly apply preferredColorScheme at root level - Added ZStack with ID to force view redraw on theme change - Implemented applyAppearance method in RootHostingController - Added debug logging to track theme changes - Synchronized UserDefaults to ensure persistence - Updated both window and hosting controller interface styles - Added notification observer for cross-component communication 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- V2er/General/RootView.swift | 20 +++++++ V2er/General/V2erApp.swift | 54 ++++++++++++++----- .../DataFlow/Reducers/SettingReducer.swift | 10 +++- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/V2er/General/RootView.swift b/V2er/General/RootView.swift index 5cc1ef4..253b5a4 100644 --- a/V2er/General/RootView.swift +++ b/V2er/General/RootView.swift @@ -27,6 +27,26 @@ class RootHostingController: UIHostingController { override var preferredStatusBarStyle: UIStatusBarStyle { return V2erApp.statusBarState } + + override func viewDidLoad() { + super.viewDidLoad() + // Apply the saved appearance mode + if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode"), + let mode = AppearanceMode(rawValue: savedMode) { + applyAppearance(mode) + } + } + + func applyAppearance(_ mode: AppearanceMode) { + switch mode { + case .light: + overrideUserInterfaceStyle = .light + case .dark: + overrideUserInterfaceStyle = .dark + case .system: + overrideUserInterfaceStyle = .unspecified + } + } } extension View { diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 767b6e4..057ff16 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -19,6 +19,20 @@ struct V2erApp: App { init() { setupApperance() + setupNotifications() + } + + private func setupNotifications() { + NotificationCenter.default.addObserver( + forName: NSNotification.Name("AppearanceDidChange"), + object: nil, + queue: .main + ) { notification in + if let appearance = notification.object as? AppearanceMode { + print("📱 Received appearance change notification: \(appearance.rawValue)") + self.updateAppearance(appearance) + } + } } private func setupApperance() { @@ -29,21 +43,32 @@ struct V2erApp: App { var body: some Scene { WindowGroup { - RootView { - RootHostView() - .environmentObject(store) - .preferredColorScheme(store.appState.settingState.appearance.colorScheme) - .onAppear { - updateNavigationBarAppearance(for: store.appState.settingState.appearance) - updateWindowInterfaceStyle(for: store.appState.settingState.appearance) - } - .onReceive(store.$appState.map(\.settingState.appearance)) { newValue in - updateNavigationBarAppearance(for: newValue) - updateWindowInterfaceStyle(for: newValue) - } + ZStack { + // Hidden view to trigger redraws + Color.clear + .frame(width: 0, height: 0) + .id(store.appState.settingState.appearance) + + RootView { + RootHostView() + .environmentObject(store) + } + } + .preferredColorScheme(store.appState.settingState.appearance.colorScheme) + .environmentObject(store) + .onAppear { + updateAppearance(store.appState.settingState.appearance) + } + .onChange(of: store.appState.settingState.appearance) { newValue in + updateAppearance(newValue) } } } + + private func updateAppearance(_ appearance: AppearanceMode) { + updateNavigationBarAppearance(for: appearance) + updateWindowInterfaceStyle(for: appearance) + } private func updateNavigationBarAppearance(for appearance: AppearanceMode) { DispatchQueue.main.async { @@ -107,6 +132,11 @@ struct V2erApp: App { if let window = V2erApp.window { window.overrideUserInterfaceStyle = style } + + // Update the root hosting controller + if let rootHostingController = V2erApp.rootViewController as? RootHostingController { + rootHostingController.applyAppearance(appearance) + } } } diff --git a/V2er/State/DataFlow/Reducers/SettingReducer.swift b/V2er/State/DataFlow/Reducers/SettingReducer.swift index ec3613e..57a3c46 100644 --- a/V2er/State/DataFlow/Reducers/SettingReducer.swift +++ b/V2er/State/DataFlow/Reducers/SettingReducer.swift @@ -11,17 +11,23 @@ import Foundation func settingStateReducer(_ state: SettingState, _ action: Action) -> (SettingState, Action?) { var state = state var followingAction: Action? = action - + switch action { case let action as SettingActions.ChangeAppearanceAction: + print("🎨 Changing appearance to: \(action.appearance.rawValue)") state.appearance = action.appearance // Save to UserDefaults UserDefaults.standard.set(action.appearance.rawValue, forKey: "appearanceMode") + UserDefaults.standard.synchronize() + + // Post notification for immediate UI update + NotificationCenter.default.post(name: NSNotification.Name("AppearanceDidChange"), object: action.appearance) + followingAction = nil default: break } - + return (state, followingAction) } From f066783f1acce49df85aba61df665c6707a431af Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sun, 14 Sep 2025 07:42:45 +0800 Subject: [PATCH 3/7] fix: resolve white screen issue on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed AppearanceMode type dependency in RootHostingController - Simplified WindowGroup structure removing ZStack wrapper - Use string-based appearance handling to avoid type issues - Restored stable app launch while keeping theme functionality 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- V2er/General/RootView.swift | 23 ++++++++++++----------- V2er/General/V2erApp.swift | 16 ++++------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/V2er/General/RootView.swift b/V2er/General/RootView.swift index 253b5a4..c2350ce 100644 --- a/V2er/General/RootView.swift +++ b/V2er/General/RootView.swift @@ -31,21 +31,22 @@ class RootHostingController: UIHostingController { override func viewDidLoad() { super.viewDidLoad() // Apply the saved appearance mode - if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode"), - let mode = AppearanceMode(rawValue: savedMode) { - applyAppearance(mode) + if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode") { + applyAppearanceFromString(savedMode) } } - func applyAppearance(_ mode: AppearanceMode) { - switch mode { - case .light: - overrideUserInterfaceStyle = .light - case .dark: - overrideUserInterfaceStyle = .dark - case .system: - overrideUserInterfaceStyle = .unspecified + func applyAppearanceFromString(_ modeString: String) { + let style: UIUserInterfaceStyle + switch modeString { + case "light": + style = .light + case "dark": + style = .dark + default: + style = .unspecified } + overrideUserInterfaceStyle = style } } diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 057ff16..4cea0ef 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -43,19 +43,11 @@ struct V2erApp: App { var body: some Scene { WindowGroup { - ZStack { - // Hidden view to trigger redraws - Color.clear - .frame(width: 0, height: 0) - .id(store.appState.settingState.appearance) - - RootView { - RootHostView() - .environmentObject(store) - } + RootView { + RootHostView() + .environmentObject(store) } .preferredColorScheme(store.appState.settingState.appearance.colorScheme) - .environmentObject(store) .onAppear { updateAppearance(store.appState.settingState.appearance) } @@ -135,7 +127,7 @@ struct V2erApp: App { // Update the root hosting controller if let rootHostingController = V2erApp.rootViewController as? RootHostingController { - rootHostingController.applyAppearance(appearance) + rootHostingController.applyAppearanceFromString(appearance.rawValue) } } } From 049a35a00211dd3e9dd03f0d5afc4014236691e2 Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sun, 14 Sep 2025 07:55:17 +0800 Subject: [PATCH 4/7] fix: improve theme switching persistence and reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed settings UI to use store state directly instead of local @State - Added comprehensive debug logging throughout theme change flow - Enhanced window interface style updates to cover all scenes and windows - Added startup theme application with slight delay for proper initialization - Improved UserDefaults loading with debug output - Force window redraw after theme changes - Ensure all UI elements properly reflect saved theme preference 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- V2er/General/V2erApp.swift | 28 +++++++++++++++---- V2er/State/DataFlow/State/SettingState.swift | 5 +++- .../View/Settings/AppearanceSettingView.swift | 8 ++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 4cea0ef..9150bbf 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -20,6 +20,12 @@ struct V2erApp: App { init() { setupApperance() setupNotifications() + // Apply saved theme on app launch + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let savedAppearance = Store.shared.appState.settingState.appearance + print("🚀 Applying saved appearance on launch: \(savedAppearance.rawValue)") + self.updateAppearance(savedAppearance) + } } private func setupNotifications() { @@ -58,6 +64,7 @@ struct V2erApp: App { } private func updateAppearance(_ appearance: AppearanceMode) { + print("🔄 Updating appearance to: \(appearance.rawValue)") updateNavigationBarAppearance(for: appearance) updateWindowInterfaceStyle(for: appearance) } @@ -102,9 +109,6 @@ struct V2erApp: App { private func updateWindowInterfaceStyle(for appearance: AppearanceMode) { DispatchQueue.main.async { - // Get all windows and update their interface style - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - let style: UIUserInterfaceStyle switch appearance { case .light: @@ -115,20 +119,32 @@ struct V2erApp: App { style = .unspecified } - // Update all windows in the scene - windowScene.windows.forEach { window in - window.overrideUserInterfaceStyle = style + print("🪟 Setting window interface style to: \(style.rawValue)") + + // Update all connected scenes + UIApplication.shared.connectedScenes.forEach { scene in + if let windowScene = scene as? UIWindowScene { + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = style + print(" ✓ Updated window: \(window)") + } + } } // Also update the stored window if available if let window = V2erApp.window { window.overrideUserInterfaceStyle = style + print(" ✓ Updated stored window") } // Update the root hosting controller if let rootHostingController = V2erApp.rootViewController as? RootHostingController { rootHostingController.applyAppearanceFromString(appearance.rawValue) + print(" ✓ Updated root hosting controller") } + + // Force a redraw + UIApplication.shared.windows.forEach { $0.setNeedsDisplay() } } } diff --git a/V2er/State/DataFlow/State/SettingState.swift b/V2er/State/DataFlow/State/SettingState.swift index 29b6e58..237e7cd 100644 --- a/V2er/State/DataFlow/State/SettingState.swift +++ b/V2er/State/DataFlow/State/SettingState.swift @@ -11,12 +11,15 @@ import SwiftUI struct SettingState: FluxState { var appearance: AppearanceMode = .system - + init() { // Load saved preference if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode"), let mode = AppearanceMode(rawValue: savedMode) { self.appearance = mode + print("📱 Loaded saved appearance: \(mode.rawValue)") + } else { + print("📱 No saved appearance, using default: system") } } } diff --git a/V2er/View/Settings/AppearanceSettingView.swift b/V2er/View/Settings/AppearanceSettingView.swift index 606d9aa..464700c 100644 --- a/V2er/View/Settings/AppearanceSettingView.swift +++ b/V2er/View/Settings/AppearanceSettingView.swift @@ -10,14 +10,10 @@ import SwiftUI struct AppearanceSettingView: View { @EnvironmentObject private var store: Store - @State private var selectedAppearance: AppearanceMode = .system var body: some View { formView .navBar("外观设置") - .onAppear { - selectedAppearance = store.appState.settingState.appearance - } } private func updateAppearance(_ mode: AppearanceMode) { @@ -56,7 +52,7 @@ struct AppearanceSettingView: View { VStack(spacing: 0) { ForEach(AppearanceMode.allCases, id: \.self) { mode in Button(action: { - selectedAppearance = mode + print("🎨 User selected: \(mode.rawValue)") dispatch(SettingActions.ChangeAppearanceAction(appearance: mode)) // Force immediate UI update updateAppearance(mode) @@ -65,7 +61,7 @@ struct AppearanceSettingView: View { Text(mode.displayName) .foregroundColor(.primaryText) Spacer() - if selectedAppearance == mode { + if store.appState.settingState.appearance == mode { Image(systemName: "checkmark") .foregroundColor(.tintColor) .font(.system(size: 14, weight: .semibold)) From 4de2b20332518c0033559acd053885e5588ea576 Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sun, 14 Sep 2025 08:59:06 +0800 Subject: [PATCH 5/7] fix: resolve compilation error with escaping closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created static methods for appearance updates to avoid self capture in init - Replaced instance method calls with static versions in closures - Simplified updateAppearance in AppearanceSettingView to use static method - Maintains all functionality while fixing Swift compilation issue 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- V2er/General/V2erApp.swift | 89 ++++++++++++++++++- .../View/Settings/AppearanceSettingView.swift | 20 +---- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 9150bbf..660e461 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -24,7 +24,7 @@ struct V2erApp: App { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { let savedAppearance = Store.shared.appState.settingState.appearance print("🚀 Applying saved appearance on launch: \(savedAppearance.rawValue)") - self.updateAppearance(savedAppearance) + V2erApp.updateAppearanceStatic(savedAppearance) } } @@ -36,7 +36,7 @@ struct V2erApp: App { ) { notification in if let appearance = notification.object as? AppearanceMode { print("📱 Received appearance change notification: \(appearance.rawValue)") - self.updateAppearance(appearance) + V2erApp.updateAppearanceStatic(appearance) } } } @@ -68,7 +68,51 @@ struct V2erApp: App { updateNavigationBarAppearance(for: appearance) updateWindowInterfaceStyle(for: appearance) } + + static func updateAppearanceStatic(_ appearance: AppearanceMode) { + print("🔄 Updating appearance to: \(appearance.rawValue)") + updateNavigationBarAppearanceStatic(for: appearance) + updateWindowInterfaceStyleStatic(for: appearance) + } + static func updateNavigationBarAppearanceStatic(for appearance: AppearanceMode) { + DispatchQueue.main.async { + let navbarAppearance = UINavigationBarAppearance() + + // Determine if we should use dark mode + let isDarkMode: Bool + switch appearance { + case .light: + isDarkMode = false + case .dark: + isDarkMode = true + case .system: + isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark + } + + let tintColor = isDarkMode ? UIColor.white : UIColor.black + navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor] + navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor] + navbarAppearance.backgroundColor = .clear + + let navAppearance = UINavigationBar.appearance() + navAppearance.standardAppearance = navbarAppearance + navAppearance.compactAppearance = navbarAppearance + navAppearance.scrollEdgeAppearance = navbarAppearance + navAppearance.backgroundColor = .clear + navAppearance.tintColor = tintColor + + // Force refresh of current navigation controllers + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + windowScene.windows.forEach { window in + window.subviews.forEach { _ in + window.tintColor = tintColor + } + } + } + } + } + private func updateNavigationBarAppearance(for appearance: AppearanceMode) { DispatchQueue.main.async { let navbarAppearance = UINavigationBarAppearance() @@ -107,6 +151,47 @@ struct V2erApp: App { } } + static func updateWindowInterfaceStyleStatic(for appearance: AppearanceMode) { + DispatchQueue.main.async { + let style: UIUserInterfaceStyle + switch appearance { + case .light: + style = .light + case .dark: + style = .dark + case .system: + style = .unspecified + } + + print("🪟 Setting window interface style to: \(style.rawValue)") + + // Update all connected scenes + UIApplication.shared.connectedScenes.forEach { scene in + if let windowScene = scene as? UIWindowScene { + windowScene.windows.forEach { window in + window.overrideUserInterfaceStyle = style + print(" ✓ Updated window: \(window)") + } + } + } + + // Also update the stored window if available + if let window = V2erApp.window { + window.overrideUserInterfaceStyle = style + print(" ✓ Updated stored window") + } + + // Update the root hosting controller + if let rootHostingController = V2erApp.rootViewController as? RootHostingController { + rootHostingController.applyAppearanceFromString(appearance.rawValue) + print(" ✓ Updated root hosting controller") + } + + // Force a redraw + UIApplication.shared.windows.forEach { $0.setNeedsDisplay() } + } + } + private func updateWindowInterfaceStyle(for appearance: AppearanceMode) { DispatchQueue.main.async { let style: UIUserInterfaceStyle diff --git a/V2er/View/Settings/AppearanceSettingView.swift b/V2er/View/Settings/AppearanceSettingView.swift index 464700c..a499314 100644 --- a/V2er/View/Settings/AppearanceSettingView.swift +++ b/V2er/View/Settings/AppearanceSettingView.swift @@ -17,24 +17,8 @@ struct AppearanceSettingView: View { } private func updateAppearance(_ mode: AppearanceMode) { - // Force immediate window interface style update - DispatchQueue.main.async { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - - let style: UIUserInterfaceStyle - switch mode { - case .light: - style = .light - case .dark: - style = .dark - case .system: - style = .unspecified - } - - windowScene.windows.forEach { window in - window.overrideUserInterfaceStyle = style - } - } + // Use the static method from V2erApp + V2erApp.updateAppearanceStatic(mode) } @ViewBuilder From 8649428665b1c548d0c3b6e9e7a63e1f9271ffb7 Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sun, 14 Sep 2025 09:10:08 +0800 Subject: [PATCH 6/7] fix: enable real-time theme switching without app restart - Added notification observer in RootHostingController to listen for theme changes - Moved preferredColorScheme modifier to the correct view hierarchy level - Removed duplicate notification handling in V2erApp - Added force redraw on theme change to ensure immediate UI update - Cleaned up redundant theme update calls in AppearanceSettingView The app now switches themes immediately when selecting Light/Dark/System modes in settings without requiring an app restart. --- V2er/General/RootView.swift | 22 +++++++++++++++++++ V2er/General/V2erApp.swift | 20 ++++------------- .../View/Settings/AppearanceSettingView.swift | 6 ----- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/V2er/General/RootView.swift b/V2er/General/RootView.swift index c2350ce..8b90fa2 100644 --- a/V2er/General/RootView.swift +++ b/V2er/General/RootView.swift @@ -34,6 +34,20 @@ class RootHostingController: UIHostingController { if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode") { applyAppearanceFromString(savedMode) } + + // Listen for appearance changes + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppearanceChange), + name: NSNotification.Name("AppearanceDidChange"), + object: nil + ) + } + + @objc private func handleAppearanceChange(_ notification: Notification) { + if let appearance = notification.object as? AppearanceMode { + applyAppearanceFromString(appearance.rawValue) + } } func applyAppearanceFromString(_ modeString: String) { @@ -47,6 +61,14 @@ class RootHostingController: UIHostingController { style = .unspecified } overrideUserInterfaceStyle = style + + // Force the view to redraw + view.setNeedsDisplay() + view.setNeedsLayout() + } + + deinit { + NotificationCenter.default.removeObserver(self) } } diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 660e461..938dbf7 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -21,24 +21,12 @@ struct V2erApp: App { setupApperance() setupNotifications() // Apply saved theme on app launch - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let savedAppearance = Store.shared.appState.settingState.appearance - print("🚀 Applying saved appearance on launch: \(savedAppearance.rawValue)") - V2erApp.updateAppearanceStatic(savedAppearance) - } + let savedAppearance = Store.shared.appState.settingState.appearance + print("🚀 Saved appearance on launch: \(savedAppearance.rawValue)") } private func setupNotifications() { - NotificationCenter.default.addObserver( - forName: NSNotification.Name("AppearanceDidChange"), - object: nil, - queue: .main - ) { notification in - if let appearance = notification.object as? AppearanceMode { - print("📱 Received appearance change notification: \(appearance.rawValue)") - V2erApp.updateAppearanceStatic(appearance) - } - } + // Notifications are now handled in RootHostingController } private func setupApperance() { @@ -52,8 +40,8 @@ struct V2erApp: App { RootView { RootHostView() .environmentObject(store) + .preferredColorScheme(store.appState.settingState.appearance.colorScheme) } - .preferredColorScheme(store.appState.settingState.appearance.colorScheme) .onAppear { updateAppearance(store.appState.settingState.appearance) } diff --git a/V2er/View/Settings/AppearanceSettingView.swift b/V2er/View/Settings/AppearanceSettingView.swift index a499314..e1eca6b 100644 --- a/V2er/View/Settings/AppearanceSettingView.swift +++ b/V2er/View/Settings/AppearanceSettingView.swift @@ -16,10 +16,6 @@ struct AppearanceSettingView: View { .navBar("外观设置") } - private func updateAppearance(_ mode: AppearanceMode) { - // Use the static method from V2erApp - V2erApp.updateAppearanceStatic(mode) - } @ViewBuilder private var formView: some View { @@ -38,8 +34,6 @@ struct AppearanceSettingView: View { Button(action: { print("🎨 User selected: \(mode.rawValue)") dispatch(SettingActions.ChangeAppearanceAction(appearance: mode)) - // Force immediate UI update - updateAppearance(mode) }) { HStack { Text(mode.displayName) From 7d6c2d840249a4b166aee5653ba3b47a35fb938e Mon Sep 17 00:00:00 2001 From: Gray Zhang Date: Sun, 14 Sep 2025 10:14:34 +0800 Subject: [PATCH 7/7] fix: address Copilot review comments - Removed all debug print statements from production code - Fixed deprecated UIApplication.shared.windows usage - Removed empty setupNotifications method - Cleaned up code formatting and unnecessary blank lines All CI checks should now pass. --- V2er/General/V2erApp.swift | 67 +++++-------------- .../DataFlow/Reducers/SettingReducer.swift | 1 - V2er/State/DataFlow/State/SettingState.swift | 3 - .../View/Settings/AppearanceSettingView.swift | 1 - 4 files changed, 15 insertions(+), 57 deletions(-) diff --git a/V2er/General/V2erApp.swift b/V2er/General/V2erApp.swift index 938dbf7..9d13efc 100644 --- a/V2er/General/V2erApp.swift +++ b/V2er/General/V2erApp.swift @@ -19,14 +19,6 @@ struct V2erApp: App { init() { setupApperance() - setupNotifications() - // Apply saved theme on app launch - let savedAppearance = Store.shared.appState.settingState.appearance - print("🚀 Saved appearance on launch: \(savedAppearance.rawValue)") - } - - private func setupNotifications() { - // Notifications are now handled in RootHostingController } private func setupApperance() { @@ -41,24 +33,19 @@ struct V2erApp: App { RootHostView() .environmentObject(store) .preferredColorScheme(store.appState.settingState.appearance.colorScheme) - } - .onAppear { + } .onAppear { updateAppearance(store.appState.settingState.appearance) - } - .onChange(of: store.appState.settingState.appearance) { newValue in + } .onChange(of: store.appState.settingState.appearance) { newValue in updateAppearance(newValue) - } - } + } } } private func updateAppearance(_ appearance: AppearanceMode) { - print("🔄 Updating appearance to: \(appearance.rawValue)") updateNavigationBarAppearance(for: appearance) updateWindowInterfaceStyle(for: appearance) } static func updateAppearanceStatic(_ appearance: AppearanceMode) { - print("🔄 Updating appearance to: \(appearance.rawValue)") updateNavigationBarAppearanceStatic(for: appearance) updateWindowInterfaceStyleStatic(for: appearance) } @@ -77,7 +64,6 @@ struct V2erApp: App { case .system: isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark } - let tintColor = isDarkMode ? UIColor.white : UIColor.black navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor] navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor] @@ -95,10 +81,7 @@ struct V2erApp: App { windowScene.windows.forEach { window in window.subviews.forEach { _ in window.tintColor = tintColor - } - } - } - } + } } } } } private func updateNavigationBarAppearance(for appearance: AppearanceMode) { @@ -115,7 +98,6 @@ struct V2erApp: App { case .system: isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark } - let tintColor = isDarkMode ? UIColor.white : UIColor.black navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor] navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor] @@ -133,10 +115,7 @@ struct V2erApp: App { windowScene.windows.forEach { window in window.subviews.forEach { _ in window.tintColor = tintColor - } - } - } - } + } } } } } static func updateWindowInterfaceStyleStatic(for appearance: AppearanceMode) { @@ -151,33 +130,25 @@ struct V2erApp: App { style = .unspecified } - print("🪟 Setting window interface style to: \(style.rawValue)") - // Update all connected scenes UIApplication.shared.connectedScenes.forEach { scene in if let windowScene = scene as? UIWindowScene { windowScene.windows.forEach { window in window.overrideUserInterfaceStyle = style - print(" ✓ Updated window: \(window)") - } - } - } - + } } } // Also update the stored window if available if let window = V2erApp.window { window.overrideUserInterfaceStyle = style - print(" ✓ Updated stored window") } - // Update the root hosting controller if let rootHostingController = V2erApp.rootViewController as? RootHostingController { rootHostingController.applyAppearanceFromString(appearance.rawValue) - print(" ✓ Updated root hosting controller") } - // Force a redraw - UIApplication.shared.windows.forEach { $0.setNeedsDisplay() } - } + UIApplication.shared.connectedScenes.forEach { scene in + if let windowScene = scene as? UIWindowScene { + windowScene.windows.forEach { $0.setNeedsDisplay() } + } } } } private func updateWindowInterfaceStyle(for appearance: AppearanceMode) { @@ -192,33 +163,25 @@ struct V2erApp: App { style = .unspecified } - print("🪟 Setting window interface style to: \(style.rawValue)") - // Update all connected scenes UIApplication.shared.connectedScenes.forEach { scene in if let windowScene = scene as? UIWindowScene { windowScene.windows.forEach { window in window.overrideUserInterfaceStyle = style - print(" ✓ Updated window: \(window)") - } - } - } - + } } } // Also update the stored window if available if let window = V2erApp.window { window.overrideUserInterfaceStyle = style - print(" ✓ Updated stored window") } - // Update the root hosting controller if let rootHostingController = V2erApp.rootViewController as? RootHostingController { rootHostingController.applyAppearanceFromString(appearance.rawValue) - print(" ✓ Updated root hosting controller") } - // Force a redraw - UIApplication.shared.windows.forEach { $0.setNeedsDisplay() } - } + UIApplication.shared.connectedScenes.forEach { scene in + if let windowScene = scene as? UIWindowScene { + windowScene.windows.forEach { $0.setNeedsDisplay() } + } } } } static func changeStatusBarStyle(_ style: UIStatusBarStyle) { diff --git a/V2er/State/DataFlow/Reducers/SettingReducer.swift b/V2er/State/DataFlow/Reducers/SettingReducer.swift index 57a3c46..eccd661 100644 --- a/V2er/State/DataFlow/Reducers/SettingReducer.swift +++ b/V2er/State/DataFlow/Reducers/SettingReducer.swift @@ -14,7 +14,6 @@ func settingStateReducer(_ state: SettingState, _ action: Action) -> (SettingSta switch action { case let action as SettingActions.ChangeAppearanceAction: - print("🎨 Changing appearance to: \(action.appearance.rawValue)") state.appearance = action.appearance // Save to UserDefaults UserDefaults.standard.set(action.appearance.rawValue, forKey: "appearanceMode") diff --git a/V2er/State/DataFlow/State/SettingState.swift b/V2er/State/DataFlow/State/SettingState.swift index 237e7cd..97a3ee5 100644 --- a/V2er/State/DataFlow/State/SettingState.swift +++ b/V2er/State/DataFlow/State/SettingState.swift @@ -17,9 +17,6 @@ struct SettingState: FluxState { if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode"), let mode = AppearanceMode(rawValue: savedMode) { self.appearance = mode - print("📱 Loaded saved appearance: \(mode.rawValue)") - } else { - print("📱 No saved appearance, using default: system") } } } diff --git a/V2er/View/Settings/AppearanceSettingView.swift b/V2er/View/Settings/AppearanceSettingView.swift index e1eca6b..fcd7791 100644 --- a/V2er/View/Settings/AppearanceSettingView.swift +++ b/V2er/View/Settings/AppearanceSettingView.swift @@ -32,7 +32,6 @@ struct AppearanceSettingView: View { VStack(spacing: 0) { ForEach(AppearanceMode.allCases, id: \.self) { mode in Button(action: { - print("🎨 User selected: \(mode.rawValue)") dispatch(SettingActions.ChangeAppearanceAction(appearance: mode)) }) { HStack {