24

How can I make a switch to change programatically to dark or light mode in my iOS app? I'm using Swift.

אורי orihpt
  • 2,358
  • 2
  • 16
  • 41
TheCesco1988
  • 280
  • 1
  • 2
  • 10

3 Answers3

48

You can override the style for single views or view controller using the overrideUserInterfaceStyle property. But since the window is also a view, you can set that on your main window to force it into light or dark mode:

window.overrideUserInterfaceStyle = .dark
Frank Rupprecht
  • 9,191
  • 31
  • 56
  • 3
    FYI: if the view controllers is added into a navigation controller, then it's the later which must have this property set. `dummyNavigation.overrideUserInterfaceStyle = .light/.dark` – Mauricio Chirino Jan 03 '21 at 15:14
  • window not defined – Markon Jan 21 '21 at 09:25
  • 2
    To override the entire app, you should be able to use UIWindow().overrideUserInterfaceStyle = UIUserInterfaceStyle.light – Jorge Zapata Aug 07 '21 at 14:21
  • 2
    @JorgeZapata just to clarify, that won't do anything. (It will apply the override to an unused instance of UIWindow.) Callers should get the window (if available) from the current Scene, which is typically but not always the first Scene in UIApplication's connectedScenes. Cheers. – Womble Aug 10 '21 at 03:14
4

I want to elaborate more on the answer provided by @Frank Schlegel.

To change theme from another view controller in your app (which is what you originally asked for, I think) you could add an observer for a UserDefaults value that will trigger the change.

I would add an enum to better represent the theme state

enum Theme: String {
    case light, dark, system

    // Utility var to pass directly to window.overrideUserInterfaceStyle
    var uiInterfaceStyle: UIUserInterfaceStyle {
        switch self {
        case .light:
            return .light
        case .dark:
            return .dark
        case .system:
            return .unspecified
        }
    }
}

In your SceneDelegate under your window initialisation you have to add this method that is triggered every time UserDefaults changes value.

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

Also, you want to remove that observer when the SceneDelegate is deinitialised, add

deinit {
    UserDefaults.standard.removeObserver(self, forKeyPath: "theme", context: nil)
}

This will place an observer for that theme value in UserDefaults.

To handle changes you need to add this method to your SceneDelegate class.

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    guard
        let change = change,
        object != nil,
        keyPath == Defaults.theme.rawValue,
        let themeValue = change[.newKey] as? String,
        let theme = Theme(rawValue: themeValue)?.uiInterfaceStyle
    else { return }

    UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: { [weak self] in
        self?.window?.overrideUserInterfaceStyle = theme
    }, completion: .none)
}

This will be executed every time theme value changes in UserDefaults and will animate the transition from a theme to another.

Now, to change your theme from other view controllers in your app you just need to change value for UserDefaults.

UserDefaults.standard.setValue(Theme.light.rawValue, forKey: "theme")
Mattia Righetti
  • 1,265
  • 1
  • 18
  • 31
3

You can use one of the observation ways, for example, Defaults lib, and then add

window.overrideUserInterfaceStyle = .dark

to

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {}
Mattia Righetti
  • 1,265
  • 1
  • 18
  • 31
nastassia
  • 807
  • 1
  • 12
  • 31