在 SwiftUI App 中实现暗模式切换
Posted
技术标签:
【中文标题】在 SwiftUI App 中实现暗模式切换【英文标题】:Implement dark mode switch in SwiftUI App 【发布时间】:2020-02-16 22:30:53 【问题描述】:我目前正在我的应用程序中查看暗模式。虽然由于我的 SwiftUI 基础,暗模式本身并没有太大的困难,但我正在努力设置独立于系统 ColorScheme 的 ColorScheme 的选项。
I found this in apples human interface guidelines 我想实现这个功能。 (链接:Human Interface Guidelines)
知道如何在 SwiftUI 中执行此操作吗?我发现了一些关于@Environment
的提示,但没有关于这个主题的更多信息。 (链接:Last paragraph)
【问题讨论】:
您链接的文章是 2016 年的,当时没有系统范围的暗模式。 您是指人机界面指南吗?它们已针对 ios 13 进行了更新,但也许您是对的,而且设置部分已经过时了。 有一个谷歌工具可以检查网站上次更改的时间,它说是在 2016 年。HIG 中有一个关于 iOS 13 更改的部分,它没有提到设置。跨度> 好吧,在这种情况下,我很抱歉我链接了一篇旧文章,但它不是代码参考。更多的是“我想复制类似的东西”参考。 别抱歉。我只想说它很旧,可能不再重要了。 【参考方案1】:单一视图
要更改单个视图的配色方案(可能是应用的主要ContentView
),您可以使用以下修饰符:
.environment(\.colorScheme, .light) // or .dark
或
.preferredColorScheme(.dark)
此外,您可以将其应用于ContentView
以使您的整个应用程序变暗!
假设您没有更改场景委托中的 ContentView
名称或 @main
整个应用程序(包括UIKit
部分和SwiftUI
)
首先您需要访问窗口以更改在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
都可以访问该窗口,因此您可以使用它将. overrideUserInterfaceStyle
值设置为您需要的任何方案。
myView.window?.overrideUserInterfaceStyle = .dark
【讨论】:
这帮助很大!谢谢!将其放入分段选择器并添加功能以使用系统 ColorScheme 的任何想法? 与此类似。唯一不同的是型号。 你的意思是ToggleModel,对吧?您将如何处理这项任务? 评论不是回答问题的地方我的朋友。但请随时ask a new question。但是这一次,请确保您阅读了how-to-ask 指南,并使用您已经尝试过的代码添加了一个最小代码;) 好吧,我不用再问了,解决方案是第三种情况:.unspecified
。非常感谢您,祝您有愉快的一天!【参考方案2】:
使用@AppStorage
切换暗模式的演示
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: $0)
.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()
【讨论】:
如果可能的话,我会为 isDarkMode 使用 Bool 而不是 int。我不熟悉 swiftUI,但这给了我 C 氛围:D 这样更好! 这太棒了!但有一件事,当系统模式较暗且应用程序较亮时,模拟器状态栏上的文本变得不可读(运营商信息等)(白底白字)。有没有办法从更改中排除栏,或更改文本的颜色? @fankibiber 代码已更新。添加.preferredColorScheme(isDarkMode ? .dark : .light)
作为一个修饰符轻松添加到整个 ContentView - 太棒了!谢谢。【参考方案3】:
@Mojtaba Hosseini's 的回答确实帮助了我,但我使用的是 iOS14 的 @main
而不是 SceneDelegate
,以及一些 UIKit
视图,所以我最终使用了这样的东西(这不会切换模式,但它确实在 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)
【讨论】:
您可以通过更简单的方式访问UIWindow?
:UIApplication.shared.windows.first
@pawello2222 感谢您的提醒,我也更新了我的答案。【参考方案4】:
使用 SwiftUI 和 SceneDelegate 生命周期的系统范围
我使用in the answer by Mojtaba Hosseini 的答案中提供的提示在 SwiftUI(具有 AppDelegate 生命周期的应用程序)中制作了我自己的版本。我还没有考虑使用 iOS14 的 @main 代替 SceneDelegate。
这里是 GitHub 存储库的链接。该示例具有浅色、深色和自动选择器,可更改整个应用的设置。 我加倍努力使其可本地化!
GitHub repo
我需要访问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。)
【讨论】:
【参考方案5】:#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
)
【讨论】:
【参考方案6】:@ADB 的答案很好,但我找到了更好的答案。希望有人能找到比我更好的:D 一旦应用切换状态(进入后台并返回),这种方法不会一遍又一遍地调用相同的函数
在您的@main
视图中添加:
ContentView()
.modifier(DarkModeViewModifier())
现在创建DarkModeViewModifier()
ViewModel:
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))
【讨论】:
以上是关于在 SwiftUI App 中实现暗模式切换的主要内容,如果未能解决你的问题,请参考以下文章
在 viewDidLoad 中使用动态颜色如何自动切换深色模式的颜色?
SwiftUI 简明教程之 Swift Package Manager 的使用