I have a custom UITextField
subclass which changes its border color when typing something in it. I'm listening for changes by calling
self.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
and then, in textFieldDidChange(_:)
I'm doing:
self.layer.borderColor = UIColor(named: "testColor")?.cgColor
Where testColor
is a color defined in Assets.xcassets with variants for light and dark mode. The issue is that UIColor(named: "testColor")?.cgColor
seems to always return the color for the light mode.
Is this a bug in iOS 13 beta or I'm doing something wrong? There's a GitHub repo with the code which exhibits this behaviour. Run the project, switch to dark mode from XCode then start typing something in the text field.
Short answer
In this situation, you need to specify which trait collection to use to resolve the dynamic color.
self.traitCollection.performAsCurrent {
self.layer.borderColor = UIColor(named: "testColor")?.cgColor
}
or
self.layer.borderColor = UIColor(named: "testColor")?.resolvedColor(with: self.traitCollection).cgColor
Longer answer
When you call the cgColor
method on a dynamic UIColor
, it needs to resolve the dynamic color's value. That is done by referring to the current trait collection, UITraitCollection.current
.
The current trait collection is set by UIKit when it calls your overrides of certain methods, notably:
- UIView
- draw()
- layoutSubviews()
- traitCollectionDidChange()
- tintColorDidChange()
- UIViewController
- viewWillLayoutSubviews()
- viewDidLayoutSubviews()
- traitCollectionDidChange()
- UIPresentationController
- containerViewWillLayoutSubviews()
- containerViewDidLayoutSubviews()
- traitCollectionDidChange()
However, outside of overrides of those methods, the current trait collection is not necessarily set to any particular value. So, if your code is not in an override of one of those methods, and you want to resolve a dynamic color, it's your responsibility to tell us what trait collection to use.
(That's because it's possible to override the userInterfaceStyle
trait of any view or view controller, so even though the device may be set to light mode, you might have a view that's in dark mode.)
You can do that by directly resolving the dynamic color, using the UIColor method resolvedColor(with:)
. Or use the UITraitCollection method performAsCurrent
, and put your code that resolves the color inside the closure. The short answer above shows both ways.
You could also move your code into one of those methods. In this case, I think you could put it in layoutSubviews()
. If you do that, it will automatically get called when the light/dark style changes, so you wouldn't need to do anything else.
Reference
WWDC 2019, Implementing Dark Mode in iOS
Starting at 19:00 I talked about how dynamic colors get resolved, and at 23:30 I presented an example of how to set a CALayer
's border color to a dynamic color, just like you're doing.
I'm going to post this as a reply since I just found what the issue is. It seems to be a bug, if I activate Dark Appearance from Simulator -> Settings -> Developer the UITextField has the expected color. The issue appears only if I set the interface style to dark from the Environment Overrides button in Xcode.