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

1

9 Answers 9

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

11
  • 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? Commented Jan 22, 2020 at 0: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.
    – hrmncd
    Commented Mar 4, 2020 at 23:16
  • 1
    I found that I actually get better behavior without that line Commented Mar 4, 2020 at 23:20
  • 1
    I also found better behavior without return like @DavidChopin pointed out
    – stepheaw
    Commented May 11, 2020 at 20:43
  • 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
    Commented Jan 28, 2021 at 14:25
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.

0
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.

2
  • 1
    you need ios 13+ Commented Aug 15, 2022 at 11:16
  • This wouldn't work for things that require cgColor Commented Oct 1 at 21:46
3

Objective-C version:

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

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

    }
}
1

in iOS Swift 5

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  // Do sonthing
}
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. Commented Sep 20, 2019 at 8:46
  • 1
    Is there a reason you're not calling the base method here?
    – stepheaw
    Commented May 11, 2020 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
    Commented May 18, 2020 at 20:42
1

iOS 17+

As of iOS 17, the traitCollectionDidChange method has been deprecated. Apple now recommends observing trait changes via one of the registerForTraitChanges APIs. See the latest documentation for more info.

registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in
    if self.traitCollection.userInterfaceStyle == .dark {
        // Dark mode
    } else {
        // Light mode
    }
}
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
        }
    }
}
-1
 override public func tintColorDidChange() {
    super.tintColorDidChange()

    if traitCollection.userInterfaceStyle == .dark {
       ...
    } else {
       ...
    }
 }
1
  • 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. Commented Mar 13 at 22:27
-3

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

3
  • 1
    Good idea. I decided to give it a try and so far work outs great.
    – Sasho
    Commented Sep 18, 2019 at 8: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 to find out more about [How to detect Light|Dark mode change in iOS 13] (stackoverflow.com/questions/58016866/…) @Sasho Commented Sep 28, 2019 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. Commented Oct 16, 2019 at 10:03

Not the answer you're looking for? Browse other questions tagged or ask your own question.