1

我目前正在尝试在应用程序中实施解决方案,用户应该能够使用以下选项实时切换应用程序的外观:

  • 系统(应用在设备的 iOS 设置中设置的任何外观)
  • 光(应用 .light 配色方案)
  • 深色(应用 .dark 配色方案)

事实证明,使用 .preferredColorScheme() 设置浅色和深色配色方案非常简单且响应迅速;但是,我还没有为“系统”选项找到任何令人满意的解决方案。

我目前的方法如下:

  1. 在 ContentView 中使用 @Environment(.colorScheme) 获取设备配色方案
  2. 创建自定义视图修改器以在任何视图上应用相应的配色方案
  3. 在“MainView”(应用程序的真实内容应该存在的地方)上使用修饰符在配色方案之间切换

我的想法是将 MainView 嵌入 ContentView 中,这样@Environment(.colorScheme) 就不会被应用于 MainView 的任何 colorScheme 干扰。

但是,它仍然不能按预期工作:设置明暗外观时,一切都按预期工作。但是,当从亮/暗切换到“系统”时,外观的变化只有在重新启动应用程序后才能看到。然而,预期的行为将是外观立即改变。

对此有什么想法吗?

以下是相关的代码片段:

主视图

import SwiftUI

struct MainView: View {

    @AppStorage("selectedAppearance") var selectedAppearance = 0

    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                selectedAppearance = 1
            }) {
                Text("Light")
            }
            Spacer()
            Button(action: {
                selectedAppearance = 2
            }) {
                Text("Dark")
            }
            Spacer()
            Button(action: {
                selectedAppearance = 0
            }) {
                Text("System")
            }
            Spacer()
        }
    }
}

内容视图

import SwiftUI

struct ContentView: View {

    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        MainView()
            .modifier(ColorSchemeModifier(colorScheme: colorScheme))
    }
}

“实用程序”

import Foundation
import SwiftUI

struct ColorSchemeModifier: ViewModifier {

    @AppStorage("selectedAppearance") var selectedAppearance: Int = 0
    var colorScheme: ColorScheme

    func body(content: Content) -> some View {
        if selectedAppearance == 2 {
            return content.preferredColorScheme(.dark)
        } else if selectedAppearance == 1 {
            return content.preferredColorScheme(.light)
        } else {
            return content.preferredColorScheme(colorScheme)
        }
    }
}
4

4 回答 4

6

我最终使用了以下解决方案,这是对@pgb 给出的答案的轻微改编:

内容视图:

struct ContentView: View {

    @AppStorage("selectedAppearance") var selectedAppearance = 0
    var utilities = Utilities()

    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                selectedAppearance = 1
            }) {
                Text("Light")
            }
            Spacer()
            Button(action: {
                selectedAppearance = 2
            }) {
                Text("Dark")
            }
            Spacer()
            Button(action: {
                selectedAppearance = 0
            }) {
                Text("System")
            }
            Spacer()
        }
        .onChange(of: selectedAppearance, perform: { value in
            utilities.overrideDisplayMode()
        })
    }
}

助手类

class Utilities {

    @AppStorage("selectedAppearance") var selectedAppearance = 0
    var userInterfaceStyle: ColorScheme? = .dark

    func overrideDisplayMode() {
        var userInterfaceStyle: UIUserInterfaceStyle

        if selectedAppearance == 2 {
            userInterfaceStyle = .dark
        } else if selectedAppearance == 1 {
            userInterfaceStyle = .light
        } else {
            userInterfaceStyle = .unspecified
        }
    
        UIApplication.shared.windows.first?.overrideUserInterfaceStyle = userInterfaceStyle
    }
}
于 2021-01-21T18:08:12.277 回答
3

它适用于

.preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil)

iOS 14.5+: 就是这样。他们让“零”工作来重置您的首选颜色方案。

iOS14:

唯一的问题是您必须重新加载应用程序才能使系统正常工作。我可以想到一个解决方法 - 当用户选择“系统”时,您首先确定什么是当前的 colorScheme,将其更改为它,然后才将 selectedAppearance 更改为 0。

  • 用户会立即看到结果,下次启动时它将成为系统主题。

编辑:

这是工作思路:

struct MainView: View {
    @Binding var colorScheme: ColorScheme
    @AppStorage("selectedAppearance") var selectedAppearance = 0

    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                selectedAppearance = 1
            }) {
                Text("Light")
            }
            Spacer()
            Button(action: {
                selectedAppearance = 2
            }) {
                Text("Dark")
            }
            Spacer()
            Button(action: {
                if colorScheme == .light {
                    selectedAppearance = 1
                }
                else {
                    selectedAppearance = 2
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
                    selectedAppearance = 0
                }
            }) {
                Text("System")
            }
            Spacer()
        }
    }
}

struct ContentView: View {
    @AppStorage("selectedAppearance") var selectedAppearance = 0
    @Environment(\.colorScheme) var colorScheme
    @State var onAppearColorScheme: ColorScheme = .light //only for iOS<14.5

    var body: some View {
        MainView(colorScheme: $onAppearColorScheme)
            .onAppear { onAppearColorScheme = colorScheme } //only for iOS<14.5
            .preferredColorScheme(selectedAppearance == 1 ? .light : selectedAppearance == 2 ? .dark : nil) }
}

有一个小问题 - >它只能作为 onAppear 而不是 onChange (不知道为什么)

于 2021-01-21T13:58:09.560 回答
2

我不相信这在 SwiftUI 中是可能的。

从文档中,使用preferredColorScheme将影响整个层次结构,从封闭的演示文稿开始:

颜色方案适用于最近的封闭演示文稿,例如弹出框或窗口。视图可以使用 colorScheme 环境值读取配色方案。

因此,这会影响您的整个视图层次结构,这意味着您不能仅针对某些视图覆盖它。相反,它会“冒泡”并更改整个窗口(或演示上下文)的配色方案。

但是,您可以将其从 , 更改为UIKit使用overrideUserInterfaceStyle,它也是一个支持.dark.light和的枚举.unspecified。未指定为系统设置。

这如何转化为您的应用程序?好吧,我认为您根本不需要视图修饰符。相反,您需要监视您UserDefaultsselectedAppearance密钥的更改,并通过更改overrideUserInterfaceStyle为适当的值来对其做出反应。

因此,根据您应用程序的其余部分的结构,无论是在您的AppDelegateSceneDelegate(您也可以使用任何其他对象,但它需要访问呈现的对象UIWindow,因此在重构时请记住这一点),您可以挂钩一个侦听器UserDefaults倾听变化。像这样的东西:

UserDefaults.standard.addObserver(self, forKeyPath: "selectedAppearance", options: [.new], context: nil)

然后覆盖observeValue以监视对默认键的更改:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard
        let userDefaults = object as? UserDefaults,
        keyPath == "selectedAppearance"
    else {
        return
    }

    switch userDefaults.integer(forKey: "selectedAppearance") {
    case 2:
        window?.overrideUserInterfaceStyle = .dark
    case 1:
        window?.overrideUserInterfaceStyle = .light
    default:
        window?.overrideUserInterfaceStyle = .unspecified
    }
}

还有一些需要改进的地方(在这个问题的上下文之外,但我认为值得一提):

  • 我会使用 anenum而不是 aninteger作为外观的有效值。
  • 给它UIKit提供一个只包含你想要的 3 个值的枚举,我会重用它,因此你不需要switchobserveValue通话中使用。
于 2021-01-20T20:08:46.127 回答
0

具有 SceneDelegate 生命周期的 SwiftUI 中的系统范围实现示例

我已经在这里回答了这个问题,但是我提供了完整的实现以及其他帖子中没有要求的系统选项,这很适合这篇文章。它的工作原理与宣传的一样(到目前为止)。

我必须进行大量研究,并且必须解决很多问题。我还没有时间考虑使用 iOS14 的 @main 而不是 SceneDelegate,但希望将来这样做。
我还想在 GitHub iOS 应用程序中添加NavigationLink一个点赞!Form与选择器一样,它看起来更好。

这是 GitHub 存储库的链接。该示例具有浅色、深色和自动选择器,可更改整个应用程序的设置。
我加倍努力使其可本地化!

GitHub 仓库

我需要访问,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
            }
        }
    }

这与苹果文档一致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 repo 上看到 gif。)

于 2021-11-12T16:34:21.500 回答