在 SwiftUI App 中实现深色模式切换
Implement dark mode switch in SwiftUI App
我目前正在研究我的应用程序中的深色模式。虽然由于我的 SwiftUI 基础,暗模式本身并没有太大的困难,但我正在努力选择独立于系统 ColorScheme 设置 ColorScheme 的选项。
I found this in apples human interface guidelines and i'd like to implement this feature. (Link: Human Interface Guidelines)
知道如何在 SwiftUI 中执行此操作吗?我发现了一些关于 @Environment
的提示,但没有关于此主题的更多信息。 (Link: Last paragraph)
单视图
要更改单个视图的配色方案(可能是应用程序的主要 ContentView
),您可以使用以下修饰符:
.environment(\.colorScheme, .light) // or .dark
或
.preferredColorScheme(.dark)
此外,您可以将它应用到 ContentView
让您的整个应用变暗!
假设您没有更改场景委托中的 ContentView
名称或 @main
整个应用程序(包括 UIKit
部分和 SwiftUI
)
首先您需要访问 window 以更改在 UIKit
中调用 UserInterfaceStyle
的应用程序 colorScheme。
我在SceneDelegate
中使用了这个:
private(set) static var shared: SceneDelegate?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Self.shared = self
...
}
然后你需要绑定一个动作到切换。所以你需要一个模型。
struct ToggleModel {
var isDark: Bool = true {
didSet {
SceneDelegate.shared?.window!.overrideUserInterfaceStyle = isDark ? .dark : .light
}
}
}
最后,你只需拨动开关:
struct ContentView: View {
@State var model = ToggleModel()
var body: some View {
Toggle(isOn: $model.isDark) {
Text("is Dark")
}
}
}
来自应用程序的 UIKit 部分
每个 UIView
都可以访问 window,因此您可以使用它来将 . overrideUserInterfaceStyle
值设置为您需要的任何方案。
myView.window?.overrideUserInterfaceStyle = .dark
使用@AppStorage
切换深色模式的demo
PS:对于全局切换,修饰符应该添加到WindowGroup/MainContentView
import SwiftUI
struct SystemColor: Hashable {
var text: String
var color: Color
}
let backgroundColors: [SystemColor] = [.init(text: "Red", color: .systemRed), .init(text: "Orange", color: .systemOrange), .init(text: "Yellow", color: .systemYellow), .init(text: "Green", color: .systemGreen), .init(text: "Teal", color: .systemTeal), .init(text: "Blue", color: .systemBlue), .init(text: "Indigo", color: .systemIndigo), .init(text: "Purple", color: .systemPurple), .init(text: "Pink", color: .systemPink), .init(text: "Gray", color: .systemGray), .init(text: "Gray2", color: .systemGray2), .init(text: "Gray3", color: .systemGray3), .init(text: "Gray4", color: .systemGray4), .init(text: "Gray5", color: .systemGray5), .init(text: "Gray6", color: .systemGray6)]
struct DarkModeColorView: View {
@AppStorage("isDarkMode") var isDarkMode: Bool = true
var body: some View {
Form {
Section(header: Text("Common Colors")) {
ForEach(backgroundColors, id: \.self) {
ColorRow(color: [=10=])
}
}
}
.toolbar {
ToolbarItem(placement: .principal) { // navigation bar
Picker("Color", selection: $isDarkMode) {
Text("Light").tag(false)
Text("Dark").tag(true)
}
.pickerStyle(SegmentedPickerStyle())
}
}
.modifier(DarkModeViewModifier())
}
}
private struct ColorRow: View {
let color: SystemColor
var body: some View {
HStack {
Text(color.text)
Spacer()
Rectangle()
.foregroundColor(color.color)
.frame(width: 30, height: 30)
}
}
}
public struct DarkModeViewModifier: ViewModifier {
@AppStorage("isDarkMode") var isDarkMode: Bool = true
public func body(content: Content) -> some View {
content
.environment(\.colorScheme, isDarkMode ? .dark : .light)
.preferredColorScheme(isDarkMode ? .dark : .light) // tint on status bar
}
}
struct DarkModeColorView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
DarkModeColorView()
}
}
}
的回答确实对我有帮助,但我使用的是 iOS14 的 @main
而不是 SceneDelegate
,还有一些 UIKit
views 所以我最终使用了这样的东西(这不会切换模式,但它确实在 SwiftUI
和 UIKit
:
之间设置了暗模式
@main
struct MyTestApp: App {
@Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
ContentView()
.accentColor(.red)
.preferredColorScheme(.dark)
}
.onChange(of: phase) { _ in
setupColorScheme()
}
}
private func setupColorScheme() {
// We do this via the window so we can access UIKit components too.
let window = UIApplication.shared.windows.first
window?.overrideUserInterfaceStyle = .dark
window?.tintColor = UIColor(Color.red)
}
}
#SwiftUI #iOS #DarkMode #ColorScheme
//you can take one boolean and set colorScheme of perticuler view accordingly such like below
struct ContentView: View {
@State var darkMode : Bool = false
var body: some View {
VStack {
Toggle("DarkMode", isOn: $darkMode)
.onTapGesture(count: 1, perform: {
darkMode.toggle()
})
}
.preferredColorScheme(darkMode ? .dark : .light)
}
}
// you can also set dark light mode of whole app such like below
struct ContentView: View {
@State var darkMode : Bool = false
var body: some View {
VStack {
Toggle("DarkMode", isOn: $darkMode)
.onTapGesture(count: 1, perform: {
darkMode.toggle()
})
}
.onChange(of: darkMode, perform: { value in
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = value ? .dark : .light
})
}
}
@ADB 的回答很好,但我找到了一个更好的。希望有人能找到比我更好的:D
一旦应用程序切换状态(进入后台并返回),这种方法不会一遍又一遍地调用相同的函数
在您的 @main
视图中添加:
ContentView()
.modifier(DarkModeViewModifier())
现在创建 DarkModeViewModifier()
视图模型:
class AppThemeViewModel: ObservableObject {
@AppStorage("isDarkMode") var isDarkMode: Bool = true // also exists in DarkModeViewModifier()
@AppStorage("appTintColor") var appTintColor: AppTintColorOptions = .indigo
}
struct DarkModeViewModifier: ViewModifier {
@ObservedObject var appThemeViewModel: AppThemeViewModel = AppThemeViewModel()
public func body(content: Content) -> some View {
content
.preferredColorScheme(appThemeViewModel.isDarkMode ? .dark : appThemeViewModel.isDarkMode == false ? .light : nil)
.accentColor(Color(appThemeViewModel.appTintColor.rawValue))
}
}
Systemwide with SwiftUI with SceneDelegate lifecycle
我使用 的答案中提供的提示在 SwiftUI 中制作了我自己的版本(具有 AppDelegate 生命周期的应用程序)。我还没有考虑使用 iOS14 的 @main 而不是 SceneDelegate。
这是 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。)
我已经使用 and combined in some of the work by 的答案来实现我自己的实现
我仍然添加@main 以及在我的设置视图中
ContentView()
.modifier(DarkModeViewModifier())
然后我有以下内容:
class AppThemeViewModel: ObservableObject {
@AppStorage("appThemeSetting") var appThemeSetting = Appearance.system
}
struct DarkModeViewModifier: ViewModifier {
@ObservedObject var appThemeViewModel: AppThemeViewModel = AppThemeViewModel()
public func body(content: Content) -> some View {
content
.preferredColorScheme((appThemeViewModel.appThemeSetting == .system) ? .none : appThemeViewModel.appThemeSetting == .light ? .light : .dark)
}
}
enum Appearance: String, CaseIterable, Identifiable {
case system
case light
case dark
var id: String { self.rawValue }
}
struct ThemeSettingsView:View{
@AppStorage("appThemeSetting") var appThemeSetting = Appearance.system
var body: some View {
HStack {
Picker("Appearance", selection: $appThemeSetting) {
ForEach(Appearance.allCases) {appearance in
Text(appearance.rawValue.capitalized)
.tag(appearance)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
工作几乎完美 - 我遇到的唯一问题是从用户选择的值切换到系统设置时它不会更新设置视图本身。从系统切换到 Dark/Light 或在深色和浅色之间切换时,设置屏幕会正常更新。
我目前正在研究我的应用程序中的深色模式。虽然由于我的 SwiftUI 基础,暗模式本身并没有太大的困难,但我正在努力选择独立于系统 ColorScheme 设置 ColorScheme 的选项。
I found this in apples human interface guidelines and i'd like to implement this feature. (Link: Human Interface Guidelines)
知道如何在 SwiftUI 中执行此操作吗?我发现了一些关于 @Environment
的提示,但没有关于此主题的更多信息。 (Link: Last paragraph)
单视图
要更改单个视图的配色方案(可能是应用程序的主要 ContentView
),您可以使用以下修饰符:
.environment(\.colorScheme, .light) // or .dark
或
.preferredColorScheme(.dark)
此外,您可以将它应用到 ContentView
让您的整个应用变暗!
假设您没有更改场景委托中的 ContentView
名称或 @main
整个应用程序(包括 UIKit
部分和 SwiftUI
)
首先您需要访问 window 以更改在 UIKit
中调用 UserInterfaceStyle
的应用程序 colorScheme。
我在SceneDelegate
中使用了这个:
private(set) static var shared: SceneDelegate?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Self.shared = self
...
}
然后你需要绑定一个动作到切换。所以你需要一个模型。
struct ToggleModel {
var isDark: Bool = true {
didSet {
SceneDelegate.shared?.window!.overrideUserInterfaceStyle = isDark ? .dark : .light
}
}
}
最后,你只需拨动开关:
struct ContentView: View {
@State var model = ToggleModel()
var body: some View {
Toggle(isOn: $model.isDark) {
Text("is Dark")
}
}
}
来自应用程序的 UIKit 部分
每个 UIView
都可以访问 window,因此您可以使用它来将 . overrideUserInterfaceStyle
值设置为您需要的任何方案。
myView.window?.overrideUserInterfaceStyle = .dark
使用@AppStorage
切换深色模式的demo
PS:对于全局切换,修饰符应该添加到WindowGroup/MainContentView
import SwiftUI
struct SystemColor: Hashable {
var text: String
var color: Color
}
let backgroundColors: [SystemColor] = [.init(text: "Red", color: .systemRed), .init(text: "Orange", color: .systemOrange), .init(text: "Yellow", color: .systemYellow), .init(text: "Green", color: .systemGreen), .init(text: "Teal", color: .systemTeal), .init(text: "Blue", color: .systemBlue), .init(text: "Indigo", color: .systemIndigo), .init(text: "Purple", color: .systemPurple), .init(text: "Pink", color: .systemPink), .init(text: "Gray", color: .systemGray), .init(text: "Gray2", color: .systemGray2), .init(text: "Gray3", color: .systemGray3), .init(text: "Gray4", color: .systemGray4), .init(text: "Gray5", color: .systemGray5), .init(text: "Gray6", color: .systemGray6)]
struct DarkModeColorView: View {
@AppStorage("isDarkMode") var isDarkMode: Bool = true
var body: some View {
Form {
Section(header: Text("Common Colors")) {
ForEach(backgroundColors, id: \.self) {
ColorRow(color: [=10=])
}
}
}
.toolbar {
ToolbarItem(placement: .principal) { // navigation bar
Picker("Color", selection: $isDarkMode) {
Text("Light").tag(false)
Text("Dark").tag(true)
}
.pickerStyle(SegmentedPickerStyle())
}
}
.modifier(DarkModeViewModifier())
}
}
private struct ColorRow: View {
let color: SystemColor
var body: some View {
HStack {
Text(color.text)
Spacer()
Rectangle()
.foregroundColor(color.color)
.frame(width: 30, height: 30)
}
}
}
public struct DarkModeViewModifier: ViewModifier {
@AppStorage("isDarkMode") var isDarkMode: Bool = true
public func body(content: Content) -> some View {
content
.environment(\.colorScheme, isDarkMode ? .dark : .light)
.preferredColorScheme(isDarkMode ? .dark : .light) // tint on status bar
}
}
struct DarkModeColorView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
DarkModeColorView()
}
}
}
@main
而不是 SceneDelegate
,还有一些 UIKit
views 所以我最终使用了这样的东西(这不会切换模式,但它确实在 SwiftUI
和 UIKit
:
@main
struct MyTestApp: App {
@Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
ContentView()
.accentColor(.red)
.preferredColorScheme(.dark)
}
.onChange(of: phase) { _ in
setupColorScheme()
}
}
private func setupColorScheme() {
// We do this via the window so we can access UIKit components too.
let window = UIApplication.shared.windows.first
window?.overrideUserInterfaceStyle = .dark
window?.tintColor = UIColor(Color.red)
}
}
#SwiftUI #iOS #DarkMode #ColorScheme
//you can take one boolean and set colorScheme of perticuler view accordingly such like below
struct ContentView: View {
@State var darkMode : Bool = false
var body: some View {
VStack {
Toggle("DarkMode", isOn: $darkMode)
.onTapGesture(count: 1, perform: {
darkMode.toggle()
})
}
.preferredColorScheme(darkMode ? .dark : .light)
}
}
// you can also set dark light mode of whole app such like below
struct ContentView: View {
@State var darkMode : Bool = false
var body: some View {
VStack {
Toggle("DarkMode", isOn: $darkMode)
.onTapGesture(count: 1, perform: {
darkMode.toggle()
})
}
.onChange(of: darkMode, perform: { value in
SceneDelegate.shared?.window?.overrideUserInterfaceStyle = value ? .dark : .light
})
}
}
@ADB 的回答很好,但我找到了一个更好的。希望有人能找到比我更好的:D 一旦应用程序切换状态(进入后台并返回),这种方法不会一遍又一遍地调用相同的函数
在您的 @main
视图中添加:
ContentView()
.modifier(DarkModeViewModifier())
现在创建 DarkModeViewModifier()
视图模型:
class AppThemeViewModel: ObservableObject {
@AppStorage("isDarkMode") var isDarkMode: Bool = true // also exists in DarkModeViewModifier()
@AppStorage("appTintColor") var appTintColor: AppTintColorOptions = .indigo
}
struct DarkModeViewModifier: ViewModifier {
@ObservedObject var appThemeViewModel: AppThemeViewModel = AppThemeViewModel()
public func body(content: Content) -> some View {
content
.preferredColorScheme(appThemeViewModel.isDarkMode ? .dark : appThemeViewModel.isDarkMode == false ? .light : nil)
.accentColor(Color(appThemeViewModel.appTintColor.rawValue))
}
}
Systemwide with SwiftUI with SceneDelegate lifecycle
我使用
这是 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。)
我已经使用
我仍然添加@main 以及在我的设置视图中
ContentView()
.modifier(DarkModeViewModifier())
然后我有以下内容:
class AppThemeViewModel: ObservableObject {
@AppStorage("appThemeSetting") var appThemeSetting = Appearance.system
}
struct DarkModeViewModifier: ViewModifier {
@ObservedObject var appThemeViewModel: AppThemeViewModel = AppThemeViewModel()
public func body(content: Content) -> some View {
content
.preferredColorScheme((appThemeViewModel.appThemeSetting == .system) ? .none : appThemeViewModel.appThemeSetting == .light ? .light : .dark)
}
}
enum Appearance: String, CaseIterable, Identifiable {
case system
case light
case dark
var id: String { self.rawValue }
}
struct ThemeSettingsView:View{
@AppStorage("appThemeSetting") var appThemeSetting = Appearance.system
var body: some View {
HStack {
Picker("Appearance", selection: $appThemeSetting) {
ForEach(Appearance.allCases) {appearance in
Text(appearance.rawValue.capitalized)
.tag(appearance)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
工作几乎完美 - 我遇到的唯一问题是从用户选择的值切换到系统设置时它不会更新设置视图本身。从系统切换到 Dark/Light 或在深色和浅色之间切换时,设置屏幕会正常更新。