在 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 视图,所以我最终使用了这样的东西(这不会切换模式,但它确实在 SwiftUIUIKit 之间设置了暗模式:

@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 中实现暗模式切换的主要内容,如果未能解决你的问题,请参考以下文章

在旧版 Android 项目上实现暗模式

在 viewDidLoad 中使用动态颜色如何自动切换深色模式的颜色?

s3c2440——swi异常

SwiftUI 简明教程之 Swift Package Manager 的使用

如何在 SwiftUI 中实现 MVVM 模式?视图不会重新渲染

SwiftUI 深色模式不适用于工作表