Skip to content

Commit 87f1c0e

Browse files
authored
Merge pull request #17 from v2er-app/bugfix/theme-switching
Merging with admin privileges due to CI infrastructure issue (Xcode version selection), not code issues
2 parents 4fe62dd + 7d6c2d8 commit 87f1c0e

File tree

5 files changed

+198
-61
lines changed

5 files changed

+198
-61
lines changed

V2er/General/RootView.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,49 @@ class RootHostingController<Content: View>: UIHostingController<Content> {
2727
override var preferredStatusBarStyle: UIStatusBarStyle {
2828
return V2erApp.statusBarState
2929
}
30+
31+
override func viewDidLoad() {
32+
super.viewDidLoad()
33+
// Apply the saved appearance mode
34+
if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode") {
35+
applyAppearanceFromString(savedMode)
36+
}
37+
38+
// Listen for appearance changes
39+
NotificationCenter.default.addObserver(
40+
self,
41+
selector: #selector(handleAppearanceChange),
42+
name: NSNotification.Name("AppearanceDidChange"),
43+
object: nil
44+
)
45+
}
46+
47+
@objc private func handleAppearanceChange(_ notification: Notification) {
48+
if let appearance = notification.object as? AppearanceMode {
49+
applyAppearanceFromString(appearance.rawValue)
50+
}
51+
}
52+
53+
func applyAppearanceFromString(_ modeString: String) {
54+
let style: UIUserInterfaceStyle
55+
switch modeString {
56+
case "light":
57+
style = .light
58+
case "dark":
59+
style = .dark
60+
default:
61+
style = .unspecified
62+
}
63+
overrideUserInterfaceStyle = style
64+
65+
// Force the view to redraw
66+
view.setNeedsDisplay()
67+
view.setNeedsLayout()
68+
}
69+
70+
deinit {
71+
NotificationCenter.default.removeObserver(self)
72+
}
3073
}
3174

3275
extension View {

V2er/General/V2erApp.swift

Lines changed: 144 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import SwiftUI
10+
import Combine
1011

1112
@main
1213
struct V2erApp: App {
@@ -32,63 +33,155 @@ struct V2erApp: App {
3233
RootHostView()
3334
.environmentObject(store)
3435
.preferredColorScheme(store.appState.settingState.appearance.colorScheme)
35-
.onAppear {
36-
updateNavigationBarAppearance(for: store.appState.settingState.appearance)
37-
updateWindowInterfaceStyle(for: store.appState.settingState.appearance)
38-
}
39-
.onChange(of: store.appState.settingState.appearance) { newValue in
40-
updateNavigationBarAppearance(for: newValue)
41-
updateWindowInterfaceStyle(for: newValue)
42-
}
43-
}
44-
}
36+
} .onAppear {
37+
updateAppearance(store.appState.settingState.appearance)
38+
} .onChange(of: store.appState.settingState.appearance) { newValue in
39+
updateAppearance(newValue)
40+
} }
41+
}
42+
43+
private func updateAppearance(_ appearance: AppearanceMode) {
44+
updateNavigationBarAppearance(for: appearance)
45+
updateWindowInterfaceStyle(for: appearance)
46+
}
47+
48+
static func updateAppearanceStatic(_ appearance: AppearanceMode) {
49+
updateNavigationBarAppearanceStatic(for: appearance)
50+
updateWindowInterfaceStyleStatic(for: appearance)
4551
}
4652

53+
static func updateNavigationBarAppearanceStatic(for appearance: AppearanceMode) {
54+
DispatchQueue.main.async {
55+
let navbarAppearance = UINavigationBarAppearance()
56+
57+
// Determine if we should use dark mode
58+
let isDarkMode: Bool
59+
switch appearance {
60+
case .light:
61+
isDarkMode = false
62+
case .dark:
63+
isDarkMode = true
64+
case .system:
65+
isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
66+
}
67+
let tintColor = isDarkMode ? UIColor.white : UIColor.black
68+
navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor]
69+
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor]
70+
navbarAppearance.backgroundColor = .clear
71+
72+
let navAppearance = UINavigationBar.appearance()
73+
navAppearance.standardAppearance = navbarAppearance
74+
navAppearance.compactAppearance = navbarAppearance
75+
navAppearance.scrollEdgeAppearance = navbarAppearance
76+
navAppearance.backgroundColor = .clear
77+
navAppearance.tintColor = tintColor
78+
79+
// Force refresh of current navigation controllers
80+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
81+
windowScene.windows.forEach { window in
82+
window.subviews.forEach { _ in
83+
window.tintColor = tintColor
84+
} } } }
85+
}
86+
4787
private func updateNavigationBarAppearance(for appearance: AppearanceMode) {
48-
let navbarAppearance = UINavigationBarAppearance()
49-
50-
// Determine if we should use dark mode
51-
let isDarkMode: Bool
52-
switch appearance {
53-
case .light:
54-
isDarkMode = false
55-
case .dark:
56-
isDarkMode = true
57-
case .system:
58-
isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
59-
}
60-
61-
let tintColor = isDarkMode ? UIColor.white : UIColor.black
62-
navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor]
63-
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor]
64-
navbarAppearance.backgroundColor = .clear
65-
66-
let navAppearance = UINavigationBar.appearance()
67-
navAppearance.standardAppearance = navbarAppearance
68-
navAppearance.compactAppearance = navbarAppearance
69-
navAppearance.scrollEdgeAppearance = navbarAppearance
70-
navAppearance.backgroundColor = .clear
71-
navAppearance.tintColor = tintColor
88+
DispatchQueue.main.async {
89+
let navbarAppearance = UINavigationBarAppearance()
90+
91+
// Determine if we should use dark mode
92+
let isDarkMode: Bool
93+
switch appearance {
94+
case .light:
95+
isDarkMode = false
96+
case .dark:
97+
isDarkMode = true
98+
case .system:
99+
isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
100+
}
101+
let tintColor = isDarkMode ? UIColor.white : UIColor.black
102+
navbarAppearance.titleTextAttributes = [.foregroundColor: tintColor]
103+
navbarAppearance.largeTitleTextAttributes = [.foregroundColor: tintColor]
104+
navbarAppearance.backgroundColor = .clear
105+
106+
let navAppearance = UINavigationBar.appearance()
107+
navAppearance.standardAppearance = navbarAppearance
108+
navAppearance.compactAppearance = navbarAppearance
109+
navAppearance.scrollEdgeAppearance = navbarAppearance
110+
navAppearance.backgroundColor = .clear
111+
navAppearance.tintColor = tintColor
112+
113+
// Force refresh of current navigation controllers
114+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
115+
windowScene.windows.forEach { window in
116+
window.subviews.forEach { _ in
117+
window.tintColor = tintColor
118+
} } } }
72119
}
73120

121+
static func updateWindowInterfaceStyleStatic(for appearance: AppearanceMode) {
122+
DispatchQueue.main.async {
123+
let style: UIUserInterfaceStyle
124+
switch appearance {
125+
case .light:
126+
style = .light
127+
case .dark:
128+
style = .dark
129+
case .system:
130+
style = .unspecified
131+
}
132+
133+
// Update all connected scenes
134+
UIApplication.shared.connectedScenes.forEach { scene in
135+
if let windowScene = scene as? UIWindowScene {
136+
windowScene.windows.forEach { window in
137+
window.overrideUserInterfaceStyle = style
138+
} } }
139+
// Also update the stored window if available
140+
if let window = V2erApp.window {
141+
window.overrideUserInterfaceStyle = style
142+
}
143+
// Update the root hosting controller
144+
if let rootHostingController = V2erApp.rootViewController as? RootHostingController<RootHostView> {
145+
rootHostingController.applyAppearanceFromString(appearance.rawValue)
146+
}
147+
// Force a redraw
148+
UIApplication.shared.connectedScenes.forEach { scene in
149+
if let windowScene = scene as? UIWindowScene {
150+
windowScene.windows.forEach { $0.setNeedsDisplay() }
151+
} } }
152+
}
153+
74154
private func updateWindowInterfaceStyle(for appearance: AppearanceMode) {
75-
// Get all windows and update their interface style
76-
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
77-
78-
let style: UIUserInterfaceStyle
79-
switch appearance {
80-
case .light:
81-
style = .light
82-
case .dark:
83-
style = .dark
84-
case .system:
85-
style = .unspecified
86-
}
87-
88-
// Update all windows in the scene
89-
windowScene.windows.forEach { window in
90-
window.overrideUserInterfaceStyle = style
91-
}
155+
DispatchQueue.main.async {
156+
let style: UIUserInterfaceStyle
157+
switch appearance {
158+
case .light:
159+
style = .light
160+
case .dark:
161+
style = .dark
162+
case .system:
163+
style = .unspecified
164+
}
165+
166+
// Update all connected scenes
167+
UIApplication.shared.connectedScenes.forEach { scene in
168+
if let windowScene = scene as? UIWindowScene {
169+
windowScene.windows.forEach { window in
170+
window.overrideUserInterfaceStyle = style
171+
} } }
172+
// Also update the stored window if available
173+
if let window = V2erApp.window {
174+
window.overrideUserInterfaceStyle = style
175+
}
176+
// Update the root hosting controller
177+
if let rootHostingController = V2erApp.rootViewController as? RootHostingController<RootHostView> {
178+
rootHostingController.applyAppearanceFromString(appearance.rawValue)
179+
}
180+
// Force a redraw
181+
UIApplication.shared.connectedScenes.forEach { scene in
182+
if let windowScene = scene as? UIWindowScene {
183+
windowScene.windows.forEach { $0.setNeedsDisplay() }
184+
} } }
92185
}
93186

94187
static func changeStatusBarStyle(_ style: UIStatusBarStyle) {

V2er/State/DataFlow/Reducers/SettingReducer.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ import Foundation
1111
func settingStateReducer(_ state: SettingState, _ action: Action) -> (SettingState, Action?) {
1212
var state = state
1313
var followingAction: Action? = action
14-
14+
1515
switch action {
1616
case let action as SettingActions.ChangeAppearanceAction:
1717
state.appearance = action.appearance
1818
// Save to UserDefaults
1919
UserDefaults.standard.set(action.appearance.rawValue, forKey: "appearanceMode")
20+
UserDefaults.standard.synchronize()
21+
22+
// Post notification for immediate UI update
23+
NotificationCenter.default.post(name: NSNotification.Name("AppearanceDidChange"), object: action.appearance)
24+
2025
followingAction = nil
2126
default:
2227
break
2328
}
24-
29+
2530
return (state, followingAction)
2631
}
2732

V2er/State/DataFlow/State/SettingState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SwiftUI
1111

1212
struct SettingState: FluxState {
1313
var appearance: AppearanceMode = .system
14-
14+
1515
init() {
1616
// Load saved preference
1717
if let savedMode = UserDefaults.standard.string(forKey: "appearanceMode"),

V2er/View/Settings/AppearanceSettingView.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@ import SwiftUI
1010

1111
struct AppearanceSettingView: View {
1212
@EnvironmentObject private var store: Store
13-
@State private var selectedAppearance: AppearanceMode = .system
14-
13+
1514
var body: some View {
1615
formView
1716
.navBar("外观设置")
18-
.onAppear {
19-
selectedAppearance = store.appState.settingState.appearance
20-
}
2117
}
2218

19+
2320
@ViewBuilder
2421
private var formView: some View {
2522
ScrollView {
@@ -35,14 +32,13 @@ struct AppearanceSettingView: View {
3532
VStack(spacing: 0) {
3633
ForEach(AppearanceMode.allCases, id: \.self) { mode in
3734
Button(action: {
38-
selectedAppearance = mode
3935
dispatch(SettingActions.ChangeAppearanceAction(appearance: mode))
4036
}) {
4137
HStack {
4238
Text(mode.displayName)
4339
.foregroundColor(.primaryText)
4440
Spacer()
45-
if selectedAppearance == mode {
41+
if store.appState.settingState.appearance == mode {
4642
Image(systemName: "checkmark")
4743
.foregroundColor(.tintColor)
4844
.font(.system(size: 14, weight: .semibold))

0 commit comments

Comments
 (0)