SwiftUI:如何让用户使用选项 "light"、"dark" 和 "system" 实时设置应用程序外观?
SwiftUI: How to let the user set the app appearance in real-time w/ options "light", "dark", and "system"?
我目前正在尝试在一个应用程序中实施一个解决方案,用户应该能够使用以下选项实时切换应用程序的外观:
- 系统(应用设备 iOS 设置中设置的任何外观)
- 浅色(应用 .light 配色方案)
- 深色(应用.深色配色方案)
已证明使用 .preferredColorScheme() 设置浅色和深色配色方案非常简单且响应迅速;但是,我还没有为“系统”选项找到任何令人满意的解决方案。
我目前的做法如下:
- 在 ContentView 中使用 @Environment(.colorScheme) 获取设备配色方案
- 创建自定义视图修改器以在任何视图上应用相应的配色方案
- 在“MainView”(这是应用程序的真实内容应该存在的地方)上使用修饰符在配色方案之间切换
我的想法是将 MainView 嵌入到 ContentView 中,这样 @Environment(.colorScheme) 就不会被应用于 MainView 的任何 colorScheme 所干扰。
然而,它仍然没有按预期工作:当设置明暗外观时,一切都按预期工作。但是,当从 light/dark 切换到“系统”时,外观的变化只有在重新启动应用程序后才能看到。然而,预期的行为是外观会立即发生变化。
对此有什么想法吗?
以下是相关的代码片段:
主视图
import SwiftUI
struct MainView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
selectedAppearance = 0
}) {
Text("System")
}
Spacer()
}
}
}
ContentView
import SwiftUI
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
MainView()
.modifier(ColorSchemeModifier(colorScheme: colorScheme))
}
}
“实用工具”
import Foundation
import SwiftUI
struct ColorSchemeModifier: ViewModifier {
@AppStorage("selectedAppearance") var selectedAppearance: Int = 0
var colorScheme: ColorScheme
func body(content: Content) -> some View {
if selectedAppearance == 2 {
return content.preferredColorScheme(.dark)
} else if selectedAppearance == 1 {
return content.preferredColorScheme(.light)
} else {
return content.preferredColorScheme(colorScheme)
}
}
}
我认为这在 SwiftUI 中是不可能的。
根据文档,使用 preferredColorScheme
将影响整个层次结构,从封闭的演示文稿开始:
The color scheme applies to the nearest enclosing presentation, such as a popover or window. Views may read the color scheme using the colorScheme environment value.
因此,这会影响您的整个视图层次结构,这意味着您不能仅针对某些视图覆盖它。相反,它“冒泡”并更改整个 window(或表示上下文)的配色方案。
但是,您可以从 UIKit
更改它,但使用 overrideUserInterfaceStyle
,它也是支持 .dark
、.light
和 .unspecified
。未指定为系统设置。
这如何转化为您的应用程序?好吧,我认为您根本不需要视图修饰符。相反,您需要监视 UserDefaults
以了解 selectedAppearance
键的更改,并通过将 overrideUserInterfaceStyle
更改为适当的值来对其做出反应。
因此,根据您的应用程序的其余部分的结构,在您的 AppDelegate
或 SceneDelegate
中(您也可以使用任何其他对象,但它需要访问所提供的UIWindow
,因此在重构时请记住这一点),您可以将侦听器挂接到 UserDefaults
以侦听更改。像这样:
UserDefaults.standard.addObserver(self, forKeyPath: "selectedAppearance", options: [.new], context: nil)
然后覆盖 observeValue
以监视对默认键的更改:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard
let userDefaults = object as? UserDefaults,
keyPath == "selectedAppearance"
else {
return
}
switch userDefaults.integer(forKey: "selectedAppearance") {
case 2:
window?.overrideUserInterfaceStyle = .dark
case 1:
window?.overrideUserInterfaceStyle = .light
default:
window?.overrideUserInterfaceStyle = .unspecified
}
}
一些其他需要改进的地方(脱离了这个问题的上下文,但我认为值得一提):
- 我会使用
enum
而不是 integer
作为外观的有效值。
- 考虑到
UIKit
提供了一个仅包含您想要的 3 个值的枚举,我会重用它,因此您不需要 observeValue
调用中的 switch
。
适用于
.preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil)
iOS 14.5+:
这就对了。他们得到了“nil”来重置您的 preferredColorScheme。
iOS14:
唯一的问题是您必须重新加载应用程序才能使系统正常工作。我可以想到一个解决方法 - 当用户选择“系统”时,您首先确定当前的 colorScheme 是什么,将其更改为它然后才将 selectedAppearance 更改为 0.
- 用户会立即看到结果,下次启动时将是系统主题。
编辑:
工作思路如下:
struct MainView: View {
@Binding var colorScheme: ColorScheme
@AppStorage("selectedAppearance") var selectedAppearance = 0
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
if colorScheme == .light {
selectedAppearance = 1
}
else {
selectedAppearance = 2
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
selectedAppearance = 0
}
}) {
Text("System")
}
Spacer()
}
}
}
struct ContentView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
@Environment(\.colorScheme) var colorScheme
@State var onAppearColorScheme: ColorScheme = .light //only for iOS<14.5
var body: some View {
MainView(colorScheme: $onAppearColorScheme)
.onAppear { onAppearColorScheme = colorScheme } //only for iOS<14.5
.preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil) }
}
有一个小问题 -> 它仅作为 onAppear 而不是 onChange 起作用(不知道为什么)
我最终使用了以下解决方案,它是对@pgb 给出的答案的轻微改编:
内容视图:
struct ContentView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var utilities = Utilities()
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
selectedAppearance = 0
}) {
Text("System")
}
Spacer()
}
.onChange(of: selectedAppearance, perform: { value in
utilities.overrideDisplayMode()
})
}
}
帮手class
class Utilities {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var userInterfaceStyle: ColorScheme? = .dark
func overrideDisplayMode() {
var userInterfaceStyle: UIUserInterfaceStyle
if selectedAppearance == 2 {
userInterfaceStyle = .dark
} else if selectedAppearance == 1 {
userInterfaceStyle = .light
} else {
userInterfaceStyle = .unspecified
}
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = userInterfaceStyle
}
}
Swift 中的系统级实现示例UI 以及 SceneDelegate 生命周期
我已经回答了这个问题 ,但是我提供了系统选项的完整实现,这在另一个 post 中没有被要求,这很适合这个 post ].它像宣传的那样工作(到目前为止)。
我必须进行大量研究并解决很多问题。我还没有时间考虑使用 iOS14 的 @main 而不是 SceneDelegate,但希望将来这样做。
我还想在 Form
中添加一个 NavigationLink
,就像在 GitHub iOS 应用程序中一样!它看起来比选择器更好。
这是 GitHub 存储库的 link。该示例具有浅色、深色和自动选择器,可更改整个应用程序的设置。
我付出了额外的努力使其可本地化!
我需要访问 SceneDelegate
并且我使用与 Mustapha 相同的代码并进行了少量添加,当应用程序启动时我需要读取存储在 UserDefaults 或 @AppStorage 等中的设置
因此我在启动时再次更新 UI:
private(set) static var shared: SceneDelegate?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Self.shared = self
// this is for when the app starts - read from the user defaults
updateUserInterfaceStyle()
}
函数 updateUserInterfaceStyle()
将在 SceneDelegate
中。
我在这里使用 UserDefaults 的扩展,使其与 iOS13 兼容(感谢 twanni!):
func updateUserInterfaceStyle() {
DispatchQueue.main.async {
switch UserDefaults.userInterfaceStyle {
case 0:
self.window?.overrideUserInterfaceStyle = .unspecified
case 1:
self.window?.overrideUserInterfaceStyle = .light
case 2:
self.window?.overrideUserInterfaceStyle = .dark
default:
self.window?.overrideUserInterfaceStyle = .unspecified
}
}
}
这与apple documentation for UIUserInterfaceStyle
一致
使用选择器意味着我需要迭代我的三个案例,所以我制作了一个符合可识别且类型为 LocalizedStringKey
的枚举用于本地化:
// check LocalizedStringKey instead of string for localisation!
enum Appearance: LocalizedStringKey, CaseIterable, Identifiable {
case light
case dark
case automatic
var id: String { UUID().uuidString }
}
这是选择器的完整代码:
struct AppearanceSelectionPicker: View {
@Environment(\.colorScheme) var colorScheme
@State private var selectedAppearance = Appearance.automatic
var body: some View {
HStack {
Text("Appearance")
.padding()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
Picker(selection: $selectedAppearance, label: Text("Appearance")) {
ForEach(Appearance.allCases) { appearance in
Text(appearance.rawValue)
.tag(appearance)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: 150, height: 50, alignment: .center)
.padding()
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding()
.onChange(of: selectedAppearance, perform: { value in
print("changed to ", value)
switch value {
case .automatic:
UserDefaults.userInterfaceStyle = 0
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .unspecified
case .light:
UserDefaults.userInterfaceStyle = 1
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .light
case .dark:
UserDefaults.userInterfaceStyle = 2
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .dark
}
})
.onAppear {
print(colorScheme)
print("UserDefaults.userInterfaceStyle",UserDefaults.userInterfaceStyle)
switch UserDefaults.userInterfaceStyle {
case 0:
selectedAppearance = .automatic
case 1:
selectedAppearance = .light
case 2:
selectedAppearance = .dark
default:
selectedAppearance = .automatic
}
}
}
}
代码 onAppear
用于在用户进入该设置视图时将滚轮设置为正确的值。每次移动轮子时,通过 .onChange
修饰符,用户默认值都会更新,应用程序会通过引用 SceneDelegate
.
更改所有视图的设置
(如果有兴趣,GH 回购中有 gif。)
我目前正在尝试在一个应用程序中实施一个解决方案,用户应该能够使用以下选项实时切换应用程序的外观:
- 系统(应用设备 iOS 设置中设置的任何外观)
- 浅色(应用 .light 配色方案)
- 深色(应用.深色配色方案)
已证明使用 .preferredColorScheme() 设置浅色和深色配色方案非常简单且响应迅速;但是,我还没有为“系统”选项找到任何令人满意的解决方案。
我目前的做法如下:
- 在 ContentView 中使用 @Environment(.colorScheme) 获取设备配色方案
- 创建自定义视图修改器以在任何视图上应用相应的配色方案
- 在“MainView”(这是应用程序的真实内容应该存在的地方)上使用修饰符在配色方案之间切换
我的想法是将 MainView 嵌入到 ContentView 中,这样 @Environment(.colorScheme) 就不会被应用于 MainView 的任何 colorScheme 所干扰。
然而,它仍然没有按预期工作:当设置明暗外观时,一切都按预期工作。但是,当从 light/dark 切换到“系统”时,外观的变化只有在重新启动应用程序后才能看到。然而,预期的行为是外观会立即发生变化。
对此有什么想法吗?
以下是相关的代码片段:
主视图
import SwiftUI
struct MainView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
selectedAppearance = 0
}) {
Text("System")
}
Spacer()
}
}
}
ContentView
import SwiftUI
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
MainView()
.modifier(ColorSchemeModifier(colorScheme: colorScheme))
}
}
“实用工具”
import Foundation
import SwiftUI
struct ColorSchemeModifier: ViewModifier {
@AppStorage("selectedAppearance") var selectedAppearance: Int = 0
var colorScheme: ColorScheme
func body(content: Content) -> some View {
if selectedAppearance == 2 {
return content.preferredColorScheme(.dark)
} else if selectedAppearance == 1 {
return content.preferredColorScheme(.light)
} else {
return content.preferredColorScheme(colorScheme)
}
}
}
我认为这在 SwiftUI 中是不可能的。
根据文档,使用 preferredColorScheme
将影响整个层次结构,从封闭的演示文稿开始:
The color scheme applies to the nearest enclosing presentation, such as a popover or window. Views may read the color scheme using the colorScheme environment value.
因此,这会影响您的整个视图层次结构,这意味着您不能仅针对某些视图覆盖它。相反,它“冒泡”并更改整个 window(或表示上下文)的配色方案。
但是,您可以从 UIKit
更改它,但使用 overrideUserInterfaceStyle
,它也是支持 .dark
、.light
和 .unspecified
。未指定为系统设置。
这如何转化为您的应用程序?好吧,我认为您根本不需要视图修饰符。相反,您需要监视 UserDefaults
以了解 selectedAppearance
键的更改,并通过将 overrideUserInterfaceStyle
更改为适当的值来对其做出反应。
因此,根据您的应用程序的其余部分的结构,在您的 AppDelegate
或 SceneDelegate
中(您也可以使用任何其他对象,但它需要访问所提供的UIWindow
,因此在重构时请记住这一点),您可以将侦听器挂接到 UserDefaults
以侦听更改。像这样:
UserDefaults.standard.addObserver(self, forKeyPath: "selectedAppearance", options: [.new], context: nil)
然后覆盖 observeValue
以监视对默认键的更改:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard
let userDefaults = object as? UserDefaults,
keyPath == "selectedAppearance"
else {
return
}
switch userDefaults.integer(forKey: "selectedAppearance") {
case 2:
window?.overrideUserInterfaceStyle = .dark
case 1:
window?.overrideUserInterfaceStyle = .light
default:
window?.overrideUserInterfaceStyle = .unspecified
}
}
一些其他需要改进的地方(脱离了这个问题的上下文,但我认为值得一提):
- 我会使用
enum
而不是integer
作为外观的有效值。 - 考虑到
UIKit
提供了一个仅包含您想要的 3 个值的枚举,我会重用它,因此您不需要observeValue
调用中的switch
。
适用于
.preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil)
iOS 14.5+: 这就对了。他们得到了“nil”来重置您的 preferredColorScheme。
iOS14:
唯一的问题是您必须重新加载应用程序才能使系统正常工作。我可以想到一个解决方法 - 当用户选择“系统”时,您首先确定当前的 colorScheme 是什么,将其更改为它然后才将 selectedAppearance 更改为 0.
- 用户会立即看到结果,下次启动时将是系统主题。
编辑:
工作思路如下:
struct MainView: View {
@Binding var colorScheme: ColorScheme
@AppStorage("selectedAppearance") var selectedAppearance = 0
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
if colorScheme == .light {
selectedAppearance = 1
}
else {
selectedAppearance = 2
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
selectedAppearance = 0
}
}) {
Text("System")
}
Spacer()
}
}
}
struct ContentView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
@Environment(\.colorScheme) var colorScheme
@State var onAppearColorScheme: ColorScheme = .light //only for iOS<14.5
var body: some View {
MainView(colorScheme: $onAppearColorScheme)
.onAppear { onAppearColorScheme = colorScheme } //only for iOS<14.5
.preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil) }
}
有一个小问题 -> 它仅作为 onAppear 而不是 onChange 起作用(不知道为什么)
我最终使用了以下解决方案,它是对@pgb 给出的答案的轻微改编:
内容视图:
struct ContentView: View {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var utilities = Utilities()
var body: some View {
VStack {
Spacer()
Button(action: {
selectedAppearance = 1
}) {
Text("Light")
}
Spacer()
Button(action: {
selectedAppearance = 2
}) {
Text("Dark")
}
Spacer()
Button(action: {
selectedAppearance = 0
}) {
Text("System")
}
Spacer()
}
.onChange(of: selectedAppearance, perform: { value in
utilities.overrideDisplayMode()
})
}
}
帮手class
class Utilities {
@AppStorage("selectedAppearance") var selectedAppearance = 0
var userInterfaceStyle: ColorScheme? = .dark
func overrideDisplayMode() {
var userInterfaceStyle: UIUserInterfaceStyle
if selectedAppearance == 2 {
userInterfaceStyle = .dark
} else if selectedAppearance == 1 {
userInterfaceStyle = .light
} else {
userInterfaceStyle = .unspecified
}
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = userInterfaceStyle
}
}
Swift 中的系统级实现示例UI 以及 SceneDelegate 生命周期
我已经回答了这个问题
我必须进行大量研究并解决很多问题。我还没有时间考虑使用 iOS14 的 @main 而不是 SceneDelegate,但希望将来这样做。
我还想在 Form
中添加一个 NavigationLink
,就像在 GitHub iOS 应用程序中一样!它看起来比选择器更好。
这是 GitHub 存储库的 link。该示例具有浅色、深色和自动选择器,可更改整个应用程序的设置。
我付出了额外的努力使其可本地化!
我需要访问 SceneDelegate
并且我使用与 Mustapha 相同的代码并进行了少量添加,当应用程序启动时我需要读取存储在 UserDefaults 或 @AppStorage 等中的设置
因此我在启动时再次更新 UI:
private(set) static var shared: SceneDelegate?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Self.shared = self
// this is for when the app starts - read from the user defaults
updateUserInterfaceStyle()
}
函数 updateUserInterfaceStyle()
将在 SceneDelegate
中。
我在这里使用 UserDefaults 的扩展,使其与 iOS13 兼容(感谢 twanni!):
func updateUserInterfaceStyle() {
DispatchQueue.main.async {
switch UserDefaults.userInterfaceStyle {
case 0:
self.window?.overrideUserInterfaceStyle = .unspecified
case 1:
self.window?.overrideUserInterfaceStyle = .light
case 2:
self.window?.overrideUserInterfaceStyle = .dark
default:
self.window?.overrideUserInterfaceStyle = .unspecified
}
}
}
这与apple documentation for UIUserInterfaceStyle
使用选择器意味着我需要迭代我的三个案例,所以我制作了一个符合可识别且类型为 LocalizedStringKey
的枚举用于本地化:
// check LocalizedStringKey instead of string for localisation!
enum Appearance: LocalizedStringKey, CaseIterable, Identifiable {
case light
case dark
case automatic
var id: String { UUID().uuidString }
}
这是选择器的完整代码:
struct AppearanceSelectionPicker: View {
@Environment(\.colorScheme) var colorScheme
@State private var selectedAppearance = Appearance.automatic
var body: some View {
HStack {
Text("Appearance")
.padding()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
Picker(selection: $selectedAppearance, label: Text("Appearance")) {
ForEach(Appearance.allCases) { appearance in
Text(appearance.rawValue)
.tag(appearance)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: 150, height: 50, alignment: .center)
.padding()
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding()
.onChange(of: selectedAppearance, perform: { value in
print("changed to ", value)
switch value {
case .automatic:
UserDefaults.userInterfaceStyle = 0
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .unspecified
case .light:
UserDefaults.userInterfaceStyle = 1
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .light
case .dark:
UserDefaults.userInterfaceStyle = 2
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = .dark
}
})
.onAppear {
print(colorScheme)
print("UserDefaults.userInterfaceStyle",UserDefaults.userInterfaceStyle)
switch UserDefaults.userInterfaceStyle {
case 0:
selectedAppearance = .automatic
case 1:
selectedAppearance = .light
case 2:
selectedAppearance = .dark
default:
selectedAppearance = .automatic
}
}
}
}
代码 onAppear
用于在用户进入该设置视图时将滚轮设置为正确的值。每次移动轮子时,通过 .onChange
修饰符,用户默认值都会更新,应用程序会通过引用 SceneDelegate
.
(如果有兴趣,GH 回购中有 gif。)