'Manually set light/dark mode in SwiftUI and save users choice

I found a solution to manually set light/dark mode in a swiftui app in another thread found here https://stackoverflow.com/a/58476468/11698443 it mostly works, but there are two problems.

  1. The users choice is not permanently saved.

  2. I would like the default choice to be dark mode, so the app will initially show up in dark mode whether the user has the system set to light or dark mode.

Right now, this implementation is a little buggy because if the user opens the app in light mode and hits the toggle switch. The first time they hit the switch will do nothing. They will have to hit the switch two more times to fire the didSet to get the app into dark mode and even then, the choice won't be saved.

A few other threads ask about dark mode implementation, but most deal with UIKit and the thread I linked to above was the only solution I could get to mostly work in swiftui. Is it possible to modify that solution to address the two issues that I brought up?



Solution 1:[1]

Here is possible approach (scratchy, you can find here on SO property wrapper for defaults and use it for better styling, but the idea to achieve your goal is the same)

Tested with Xcode 11.4 / iOS 13.4

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    private(set) static var shared: SceneDelegate?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        Self.shared = self

        let contentView = ContentView()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)

            // restore from defaults initial or previously stored style
            let style = UserDefaults.standard.integer(forKey: "LastStyle")
            window.overrideUserInterfaceStyle = (style == 0 ? .dark : UIUserInterfaceStyle(rawValue: style)!)

            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

   ...
}


struct ContentView: View {
    var body: some View {
         // manipulate with style directly in defaults
         Toggle(isOn: Binding<Bool>(
            get: { UserDefaults.standard.integer(forKey: "LastStyle") !=
                        UIUserInterfaceStyle.light.rawValue },
            set: {
                SceneDelegate.shared?.window!.overrideUserInterfaceStyle = $0 ? .dark : .light
                UserDefaults.standard.setValue($0 ? UIUserInterfaceStyle.dark.rawValue : UIUserInterfaceStyle.light.rawValue, forKey: "LastStyle")
            }
         )) {
             Text("is Dark")
        }
    }
}

backup

Solution 2:[2]

PStamatiou.

This worked for me when not using a scene delegate and only having the content view:

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.colorScheme, .light)
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 addzo