SwiftUI:如何让用户使用“亮”、“暗”和“系统”选项实时设置应用程序外观?

问题描述

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

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

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

我目前的方法如下:

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

我的想法是在 ContentView 中嵌入 MainView,这样@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)
        }
    }
}

解决方法

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

内容视图:

#include <Windows.h>
#include <iostream>

HHOOK hMouseHook;

LRESULT CALLBACK mouseProc(int nCode,WPARAM wParam,LPARAM lParam)
{   
    MOUSEHOOKSTRUCT* pMouseStruct = (MOUSEHOOKSTRUCT*)lParam;
    if (pMouseStruct != NULL)
        printf("X: %d  Y: %d\n",pMouseStruct->pt.x,pMouseStruct->pt.y);
    return CallNextHookEx(hMouseHook,nCode,wParam,lParam);
}

int main() {
    hMouseHook = SetWindowsHookEx(WH_MOUSE_LL,mouseProc,0);
    MSG message;
    while (GetMessage(&message,NULL,0)) {
        TranslateMessage(&message);
        DispatchMessage(&message);
    }

    UnhookWindowsHookEx(hMouseHook);
    return 0;
}

助手类

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()
        })
    }
}
,

我认为这在 SwiftUI 中是不可能的。

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

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

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

然而,您可以从 UIKit 更改它,但使用 overrideUserInterfaceStyle,它也是一个支持 .dark.light 和 {{1} }.未指定为系统设置。

这对您的应用有何影响?好吧,我认为您根本不需要视图修饰符。相反,您需要监视 .unspecified 以了解 UserDefaults 键的更改,并通过将 selectedAppearance 更改为适当的值来对其做出反应。

因此,取决于您的应用程序其余部分的结构,在您的 overrideUserInterfaceStyleAppDelegate 中(您也可以使用任何其他对象,但它需要访问呈现的 {{ 1}},因此在重构时请记住这一点),您可以将侦听器连接到 SceneDelegate 以侦听更改。像这样:

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 } }
  • 假设 enum 提供了一个只包含您想要的 3 个值的枚举,我会重用它,因此您不需要 integer 调用中的 UIKit
,

适用于

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

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

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(不知道为什么)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...