How to implement a color scheme switch with the system value option?

1.8k Views Asked by At

I have implemented a dark/light mode switch in my app using the guide here on this thread. Sample code below:

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

And to call it:

Picker("Color", selection: $isDarkMode) {
    Text("Light").tag(false)
    Text("Dark").tag(true)
}
.pickerStyle(SegmentedPickerStyle())

How to implement this with an addition of a System segment? I thought of setting an Int as a default setting, but I cannot figure out how to tie it with the @AppStorage property wrapper.

And also how does watching system mode changes come into effect here in SwiftUI?

Update: In iOS 15, it looks like windows is deprecated. How to update it for iOS 15 in the most sane way? I've seen some other solutions for isKeyWindow, but not sure how to apply it here.

2

There are 2 best solutions below

0
On BEST ANSWER

Thanks to @diogo for his solution. I have adapted it for ios 15 into a custom view which could be used in a settings page:

struct DisplayModeSetting: View {

    enum DisplayMode: Int {
        case system, dark, light
        
        var colorScheme: ColorScheme? {
            switch self {
            case .system: return nil
            case .dark: return ColorScheme.dark
            case .light: return ColorScheme.light
            }
        }
        
        func setAppDisplayMode() {
            var userInterfaceStyle: UIUserInterfaceStyle
            switch self {
            case .system: userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
            case .dark: userInterfaceStyle = .dark
            case .light: userInterfaceStyle = .light
            }
            let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            scene?.keyWindow?.overrideUserInterfaceStyle = userInterfaceStyle
        }
    }
    
    @AppStorage("displayMode") var displayMode = DisplayMode.system
    
    var body: some View {
        HStack {
            Text("Display mode:")
            Picker("Is Dark?", selection: $displayMode) {
                Text("System").tag(DisplayMode.system)
                Text("Dark").tag(DisplayMode.dark)
                Text("Light").tag(DisplayMode.light)
            }
            .pickerStyle(SegmentedPickerStyle())
            .onChange(of: displayMode) { newValue in
                print(displayMode)
                displayMode.setAppDisplayMode()
            }
        }
    }
}
5
On

To accomplish this, you will need to store the user's display preference from a Bool to a custom enum. Then, from this custom enum, you can determine whether the appearance should be dark or light, and apply the display preferences based on that.

Sample code:

struct ContentView: View {
    enum DisplayMode: Int {
        case system = 0
        case dark = 1
        case light = 2
    }

    @AppStorage("displayMode") var displayMode: DisplayMode = .system

    func overrideDisplayMode() {
        var userInterfaceStyle: UIUserInterfaceStyle

        switch displayMode {
        case .dark: userInterfaceStyle = .dark
        case .light: userInterfaceStyle = .light
        case .system: userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
        }

        UIApplication.shared.windows.first?.overrideUserInterfaceStyle = userInterfaceStyle
    }


    var body: some View {
        VStack {
            Picker("Color", selection: $displayMode) {
                Text("System").tag(DisplayMode.system)
                Text("Light").tag(DisplayMode.light)
                Text("Dark").tag(DisplayMode.dark)
            }
            .pickerStyle(SegmentedPickerStyle())
            .onReceive([self.displayMode].publisher.first()) { _ in
                overrideDisplayMode()
            }
        }.onAppear(perform: overrideDisplayMode)
    }
}

Basically, what you are doing is

  • assigning each display mode an integer value (so it can be stored in @AppStorage)
  • setting up the picker to choose between system, dark, and light, and saving the value in UserDefaults
  • determining whether the app is in dark mode, by switching on the @AppStorage value
  • passing the custom dark mode configuration through the views and subviews by using UIApplication.shared.windows.first?.overrideInterfaceStyle