66

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

Sign up to request clarification or add additional context in comments.

12 Comments

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?
@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.
I found that I actually get better behavior without that line
I also found better behavior without return like @DavidChopin pointed out
@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.
|
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.

Comments

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 Comments

you need ios 13+
This wouldn't work for things that require cgColor
3

Objective-C version:

if (@available(iOS 12.0, *)) { if( self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ){ //is dark }else{ //is light } } 

Comments

2

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 } } 

Comments

1

in iOS Swift 5

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { // Do sonthing } 

3 Comments

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.
Is there a reason you're not calling the base method here?
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.)
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 } } } 

Comments

-1
 override public func tintColorDidChange() { super.tintColorDidChange() if traitCollection.userInterfaceStyle == .dark { ... } else { ... } } 

1 Comment

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

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

3 Comments

Good idea. I decided to give it a try and so far work outs great.
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
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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.