Detecting iOS Dark Mode Change
Asked Answered
C

8

65

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

Credits answered 29/8, 2019 at 9:2 Comment(1)
Check this answer out.Inesinescapable
B
96

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

Burgenland answered 1/10, 2019 at 7:57 Comment(11)
UIUserInterfaceStyle is available on iOS 12 and above. I think we can even use this on iOS 12Monamonachal
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?Drin
@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.Impede
I found that I actually get better behavior without that lineDrin
@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.Burgenland
@Teo , App works fine without the "guard" statement however it works even finer with it as explained in my other comment.Burgenland
I also found better behavior without return like @DavidChopin pointed outCovenant
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?Biff
@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.Overside
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.Suzysuzzy
FYI: Make sure in Info.plist you didn't hardcoded the Appearance property, otherwise the traitCollectionDidChange won't be called.Smalltime
B
25

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.

Backer answered 29/1, 2020 at 13:11 Comment(0)
N
12

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.

Neilneila answered 31/10, 2019 at 15:11 Comment(1)
you need ios 13+Zampino
G
3

Objective-C version:

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

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

    }
}
Gaseous answered 28/2, 2020 at 11:48 Comment(0)
P
1

in iOS Swift 5

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  // Do sonthing
}
Puckett answered 19/9, 2019 at 14:42 Comment(3)
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.Credits
Is there a reason you're not calling the base method here?Covenant
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.)Nairobi
T
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
        }
    }
}
Tigress answered 18/4, 2023 at 6:47 Comment(0)
A
0
 override public func tintColorDidChange() {
    super.tintColorDidChange()

    if traitCollection.userInterfaceStyle == .dark {
       ...
    } else {
       ...
    }
 }
Anette answered 13/3 at 7:27 Comment(1)
Welcome to Stack Overflow! When posting a new answer on an existing question with multiple, pre-existing answers, it's best to provide an explanation of why yours is a better solution. While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value.Exterior
C
-2

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

Credits answered 2/9, 2019 at 12:14 Comment(3)
Good idea. I decided to give it a try and so far work outs great.Burgenland
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 to find out more about [How to detect Light|Dark mode change in iOS 13] (#58017366) @BurgenlandInesinescapable
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.Credits

© 2022 - 2024 — McMap. All rights reserved.