58

I read through the documentation regarding: https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface

When the user changes the system appearance, the system automatically asks each window and view to redraw itself. During this process, the system calls several well-known methods for both macOS and iOS, listed in the following table, to update your content.

In our legacy app we create our views as lazy variables in the init of each class. This means the views won't get drawn out with the correct color if the user goes into settings and switches to dark mode.

If you make appearance-sensitive changes outside of these methods, your app may not draw its content correctly for the current environment. The solution is to move your code into these methods.

Our application is quite big and a refactor will be done to support this in a better way in the future but I'm wondering if there is a way to detect this changes with the notification center like what can be done for Mac OS:

How to detect switch between macOS default & dark mode using Swift 3

andromedainiative
  • 4,414
  • 6
  • 22
  • 34

7 Answers7

89

Swift 5:

traitCollectionDidChange also gets called a few times. This is how I detect DarkMode runtime change and setColors().

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        setColors()
    }

In setColors() func I update the colors. Detecting current colorScheme:

extension UIViewController {
    var isDarkMode: Bool {
        if #available(iOS 13.0, *) {
            return self.traitCollection.userInterfaceStyle == .dark
        }
        else {
            return false
        }
    }

}

I have colors defined like this (for iOS < 13):

enum ColorCompatibility {
    static var myOlderiOSCompatibleColorName: UIColor {
        if UIViewController().isDarkMode {
            return UIColor(red: 33, green: 35, blue: 37, alpha: 0.85)
        }
        else {
            return UIColor(hexString: "#F3F3F3", alpha: 0.85)
        }
    }
}

Example:

private func setColors() {
  myView.backgroundColor = ColorCompatibility.myOlderiOSCompatibleColorName
}

Also you might need to call setColors in ViewDidLoad/Will/DidAppear depending on your case like this:

viewDidLoad() {
...
setColors()
...
}

For iOS11+ you could use "named Colors", defined in Assets and much easier to use in IB.

Cheers

Sasho
  • 3,532
  • 1
  • 31
  • 30
  • UIUserInterfaceStyle is available on iOS 12 and above. I think we can even use this on iOS 12 – nr5 Dec 16 '19 at 14:15
  • 1
    Can somebody explain the purpose of `guard UIApplication.shared.applicationState == .inactive else`? Does this prevent the method from being called multiple times? If so, how? – David Chopin Jan 22 '20 at 00:50
  • 1
    @DavidChopin That line causes the func to early return if application is not in inactive state. I don't need that line and func works for me perfectly without it. – Luke Mar 04 '20 at 23:16
  • 1
    I found that I actually get better behavior without that line – David Chopin Mar 04 '20 at 23:20
  • @DavidChopin when leaving the app (e.g. to change the dark mode) and coming back to the app "traitCollectionDidChange()" gets called several times. The "guard" statement makes sure the relevant code is executed only once at the right time to avoid unnecessary CPU/GPU calls and thus unnecessary screen updates. – Sasho Mar 09 '20 at 13:19
  • @Teo , App works fine without the "guard" statement however it works even finer with it as explained in my other comment. – Sasho Mar 09 '20 at 13:20
  • 1
    I also found better behavior without return like @DavidChopin pointed out – stepheaw May 11 '20 at 20:43
  • The problem with this is that it won't get called until the user returns to the app (after toggling theme in settings). So then the user sees a flash of change between themes. Is there any way to execute the code before the user returns to the app? – MobileMon Jan 18 '21 at 17:36
  • 1
    @DavidChopin I use `traitCollectionDidChange` to update `CGColors` on dark mode change. If you have two tabs, change dark mode, switch to previously not selected tab, then `UIApplication.shared.applicationState` is not `.inactive` . This means early `return` with the `guard` will not update the screen and the user will see the old colors. I will omit the `guard` now. – thetrutz Jan 28 '21 at 14:25
  • 1
    Good answer Sasho. One thing you could add is to check `hasDifferentColorAppearance` in `traitCollectionDidChange` to see if it is actually necessary to call `setColors()` - otherwise you could be setting colours whenever the other properties of the trait collection change. See Hardik's answer below. – siburb Feb 03 '21 at 09:16
23

Just override method form iOS 13 to Detect dark light mode change swift 5.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    if #available(iOS 13.0, *) {
        if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            if traitCollection.userInterfaceStyle == .dark {
                //Dark
            }
            else {
                //Light
            }
        }
    } else {
        // Fallback on earlier versions
    }
}

traitCollectionDidChange is a method in ViewControllers and Views.

Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81
10

I think for colors is better to use

UIColor.init { (trait) -> UIColor in

    return trait.userInterfaceStyle == .dark ? .label : .black
}

because this way if the system change, the color change too automatically.

3

Objective-C version:

if (@available(iOS 12.0, *)) {

    if( self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ){
       //is dark
    }else{
        //is light

    }
}
Zouhair Sassi
  • 1,403
  • 1
  • 13
  • 30
1

in iOS Swift 5

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  // Do sonthing
}
Andres Marin
  • 251
  • 3
  • 12
  • I tried to use this quite a bit but ended up with unreliable results. It got triggered twice for each change and the second value would always be the previous one. It seemed fairly wonky to me. – andromedainiative Sep 20 '19 at 08:46
  • 1
    Is there a reason you're not calling the base method here? – stepheaw May 11 '20 at 20:44
  • 1
    If using this, make sure to call `super.traitCollectionDidChange(previousTraitCollection)` as first line of function! (@andromedainiative, that might have saved you from those unreliable results.) – leanne May 18 '20 at 20:42
0

If anyone, the application is bothered by calling traitCollectionDidChange twice when it is thrown into the background, the following code block will help. Inactive state is first step for foreground(active) state. So you can handle theme changes at right time.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    let userInterfaceStyle = traitCollection.userInterfaceStyle

    if UIApplication.shared.applicationState == .inactive {
        
        switch userInterfaceStyle {
        case .unspecified:
            print("unspecified")
        case .light:
            //Do something for light mode.
            print("Light Mode")
        case .dark:
            //Do something for dark mode.
            print("Dark Mode")
        @unknown default:
            break
        }
    }
}
Okan TEL
  • 26
  • 3
-2

I ended up moving all my color setup to layoutSubviews() function in all views and the viewDidLayoutSubviews() in the view controllers.

andromedainiative
  • 4,414
  • 6
  • 22
  • 34
  • 1
    Good idea. I decided to give it a try and so far work outs great. – Sasho Sep 18 '19 at 08:38
  • 8
    `layoutSubviews` is **NOT** good place to do this. because this method is calling massively for almost **Any changes**, not only the *dark mode*, look at [this answer](https://stackoverflow.com/a/58017164/5623035) to find out more about [How to detect Light|Dark mode change in iOS 13] (https://stackoverflow.com/questions/58016866/how-to-detect-light-dark-mode-change-in-ios-13) @Sasho – Mojtaba Hosseini Sep 28 '19 at 18:43
  • Your answer is incredibly misleading, the greyed out methods and greens you are referring to have absolutely nothing to do if they are good or bad. They are indicators of when they are relevant to look at in the presentation and that is why the other methods are greyed out. – andromedainiative Oct 16 '19 at 10:03