在SwiftUI应用程序中实现黑暗模式切换

13z8s7eq  于 2022-10-31  发布在  Swift
关注(0)|答案(8)|浏览(368)

我目前正在研究我的应用程序中的黑暗模式。虽然黑暗模式本身并不是什么难事,因为我的SwiftUI基础,我正在与设置独立于系统ColorScheme的ColorScheme选项斗争。
I found this in apples human interface guidelines,我想实现此功能。(链接:(x、x、e、f、x)
你知道如何在SwiftUI中实现这一点吗?我找到了一些关于@Environment的提示,但没有关于这个主题的进一步信息。(链接:(Last paragraph

mhd8tkvw

mhd8tkvw1#

单一视图

要更改单个视图的配色方案(可能是应用的主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
yh2wf1be

yh2wf1be2#

使用@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()
        }
    }
}

jckbn6z7

jckbn6z73#

@Mojtaba Hosseini的回答确实帮助了我,但我使用的是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)
    }
}
fae0ux8s

fae0ux8s4#

@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))
    }
}
xpcnnkqh

xpcnnkqh5#

在系统范围内使用SwiftUI和SceneDelegate生命周期

我使用了in the answer by Mojtaba Hosseini在回答中提供的提示,在SwiftUI(具有AppDelegate生命周期的应用程序)中创建了我自己的版本。
这里有一个GitHub repo的链接。这个例子有light,dark和automatic picker来改变整个应用程序的设置。
而且我还多花了一点时间让它可以本地化!
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的扩展,使其与iOS 13兼容(多亏了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一致
使用选取器意味着我需要迭代我的三个case,所以我创建了一个枚举,它符合identified,并且本地化的类型为LocalizedStringKey

// check LocalizedStringKey instead of string for localisation!
enum Appearance: LocalizedStringKey, CaseIterable, Identifiable {
    case light
    case dark
    case automatic

    var id: String { UUID().uuidString }
}

下面是picker的完整代码:

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

a5g8bdjr

a5g8bdjr6#


# 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
        })

    }
}
nzk0hqpo

nzk0hqpo7#

我已经使用了@Arturo的答案,并结合了@ multudes的一些工作,来实现我自己的实现
我仍然在设置视图中添加@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())
        }
    }
}

工作几乎完美-我唯一的问题是当从用户选择的值切换到系统设置时,它不会更新设置视图本身。当从系统切换到暗/亮或在暗和亮之间切换时,它的设置屏幕确实更新得很好。

guicsvcw

guicsvcw8#

让我们试着让生活变得更简单,
在初始应用程序启动代码中,添加以下内容,

@main
struct MyAwesomeApp: App {
    @AppStorage("appearance") var appearance: String = "system"

    var body: some Scene {
        WindowGroup {
            StartView()
                .preferredColorScheme(appearance == "system" ? nil : (appearance == "dark" ? .dark : .light))
        }
    }
}

现在你可以单独设置AppearanceView并可以进行以下操作,

struct AppearanceView: View {
    @AppStorage("appearance") var appearance: String = "system"

    var body: some View {
        VStack {
            ScrollView(showsIndicators: false) {
                ForEach(Appearance.allCases, id: \.self) { appearance in
                    Button {
                        self.appearance = appearance.rawValue
                    } label: {
                        HStack {
                            Text(LocalizedStringKey(appearance.rawValue))

                            Spacer()

                            Image(self.appearance == appearance.rawValue ? "check-selected" : "check-unselected")
                        }
                    }
                }
            }
        }
    }

}

和外观枚举,

enum Appearance: String, CaseIterable {
    case light = "light"
    case dark = "dark"
    case system = "system"
}

相关问题