1

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.

fankibiber
  • 729
  • 1
  • 6
  • 19
  • 1
    For what reason are you doing it this way? What is wrong with just getting the `colorScheme` environment? – George Sep 22 '20 at 18:41
  • I am not sure how I can return only `colorScheme` from the segmented picker, whilst the value to read is `isDarkMode`. – fankibiber Sep 23 '20 at 09:05
  • Why not use the global dark mode in the user's settings? They have already chosen which mode to use – George Sep 23 '20 at 15:43
  • You mean regardless of the system setting? Well, one reason is user might want to use the dynamic setting which changes the scheme after sunset. – fankibiber Sep 23 '20 at 17:44
  • That it is already a system setting. I would just stick with the user preferences from settings. – George Sep 23 '20 at 20:46

2 Answers2

5

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
diogo
  • 511
  • 4
  • 5
  • Thanks for the detailed reply. When I try the above code in a new project, system setting is honoured with system light and dark modes, but the other choices do not work. In light mode Dark, in dark mode Light setting disables the segmented picker. – fankibiber Sep 25 '20 at 11:03
  • I just checked, and it turns out `environment` only updates SwiftUI components. As a workaround, I updated the code to override the user interface style in the whole UIView component. This should work properly now. – diogo Sep 25 '20 at 22:11
  • Ah, `UITraitCollection`! Tomorrow will try it out, and green tick it! Thank you very much! – fankibiber Sep 25 '20 at 22:17
  • 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. – fankibiber Oct 21 '21 at 19:04
  • Using `.unspecified` instead of `UITraitCollection.current.userInterfaceStyle` worked correctly. (In my limited testing, `UITraitCollection.current.userInterfaceStyle` seemed to be the value of the current style in the app, rather than the current style in the system that was set in iOS Settings.) – nishanthshanmugham May 21 '22 at 03:41
3

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()
            }
        }
    }
}
apps2go
  • 86
  • 1
  • 6