SwiftUI应用实现系统/浅色/深色模式实时切换适配iOS15+
iOS 15+ SwiftUI 实时切换系统/浅色/深色外观实现方案
旧教程中提供的单窗口样式修改方案,在iOS 15及以上版本因系统多场景管理机制变更无法实时生效,核心问题是没有覆盖所有激活状态的窗口实例,以下是可直接落地的兼容方案:
核心实现步骤
- 第一步:定义外观模式枚举与映射规则
枚举统一管理三种模式的展示文案、存储值、对应UIKit样式属性,方便后续统一维护:enum AppAppearance: Int, CaseIterable, Identifiable { case system = 0 case light = 1 case dark = 2 var id: Int { rawValue } var displayName: String { switch self { case .system: return "跟随系统" case .light: return "浅色模式" case .dark: return "深色模式" } } var uiStyle: UIUserInterfaceStyle { switch self { case .system: return .unspecified case .light: return .light case .dark: return .dark } } } - 第二步:创建全局可观测的外观管理单例
核心适配点:切换样式时遍历当前所有激活场景下的全部window更新属性,保证所有页面实时刷新,同时通过@AppStorage持久化用户选择:import SwiftUI class AppearanceManager: ObservableObject { @AppStorage("app_selected_appearance") private var storedValue: Int = AppAppearance.system.rawValue @Published var currentAppearance: AppAppearance static let shared = AppearanceManager() private init() { currentAppearance = AppAppearance(rawValue: storedValue) ?? .system } func switchTo(_ appearance: AppAppearance) { currentAppearance = appearance storedValue = appearance.rawValue // 兼容iOS15+多场景:遍历所有激活的window场景更新样式 guard let allScenes = UIApplication.shared.connectedScenes as? Set<UIWindowScene> else { return } allScenes.forEach { scene in scene.windows.forEach { window in window.overrideUserInterfaceStyle = appearance.uiStyle } } } } - 第三步:应用入口注入配置,冷启动自动加载保存的样式
在App结构体的初始化方法中提前应用已保存的样式,避免冷启动闪屏,同时将管理类注入全局环境:@main struct CustomAppearanceApp: App { @StateObject private var manager = AppearanceManager.shared init() { // 启动时立刻应用用户保存的样式 AppearanceManager.shared.switchTo(AppearanceManager.shared.currentAppearance) } var body: some Scene { WindowGroup { ContentView() .environmentObject(manager) } } } - 第四步:设置页添加切换入口
绑定选择器的值,用户选择后立刻调用切换方法实现实时更新:struct SettingPage: View { @EnvironmentObject private var manager: AppearanceManager var body: some View { Form { Section("外观设置") { Picker("界面模式", selection: Binding( get: { manager.currentAppearance }, set: { newMode in manager.switchTo(newMode) } )) { ForEach(AppAppearance.allCases) { mode in Text(mode.displayName).tag(mode) } } .pickerStyle(.inline) } } .navigationTitle("设置") } }
避坑说明
- 不要使用根视图的
.preferredColorScheme修饰符实现切换,该修饰符在iOS15+存在样式优先级冲突,系统样式变动时会覆盖用户手动选择的配置 - 不要只修改
UIApplication.shared.windows.first的样式,iOS15+该属性返回的窗口列表不全,多窗口场景下会出现部分页面不更新的问题 - 不要将样式切换逻辑写在视图的
.onAppear中,会导致应用切后台返回时样式被重置
内容的提问来源于stack exchange,提问作者user17745324
相关产品推荐
相关产品推荐

