在 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 所以我最终使用了这样的东西(这不会切换模式,但它确实在 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)
    }
}
#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。该示例具有浅色、深色和自动选择器,可更改整个应用程序的设置。
我付出了额外的努力使其可本地化!

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。)

我已经使用 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 或在深色和浅色之间切换时,设置屏幕会正常更新。