Get Light or Dark Variant of a Color Declared in Assets

Get light or dark variant of a color declared in assets

In SwiftUI if there is need to use light variant for some subview it is enough to force specify .colorScheme for it, like below

Color variants:

colors

Demo:

demo

var body: some View {
VStack {
Rectangle().fill(Color("testColor"))
.frame(width: 100, height: 100)
.environment(\.colorScheme, .light) // << force light
}
.frame(width: 300, height: 300)
.background(Color("testColor")) // << system appearance
}

How to programmatically get asset catalog color variants from Xcode 13 in SwiftUI / Swift5.5?

Thanks to @Asperi for pointing out https://stackoverflow.com/a/66950858/12299030.

TL;DR is, you can get the light and dark variants using UIColor.resolvedColor() like so:

let c = Color(UIColor(named: "hkMagenta")!.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark)))

How do I easily support light and dark mode with a custom color used in my app?

As it turns out, this is really easy with the new UIColor init(dynamicProvider:) initializer.

Update the custom color to:

extension UIColor {
static var myControlBackground: UIColor {
if #available(iOS 13.0, *) {
return UIColor { (traits) -> UIColor in
// Return one of two colors depending on light or dark mode
return traits.userInterfaceStyle == .dark ?
UIColor(red: 0.5, green: 0.4, blue: 0.3, alpha: 1) :
UIColor(red: 0.3, green: 0.4, blue: 0.5, alpha: 1)
}
} else {
// Same old color used for iOS 12 and earlier
return UIColor(red: 0.3, green: 0.4, blue: 0.5, alpha: 1)
}
}
}

That's it. No need to define two separate statics. The control class doesn't need any changes from the original code. No need to override traitCollectionDidChange or anything else.

The nice thing about this is that you can see the color change in the app switcher immediately after changing the mode in the Settings app. And of course the color is up-to-date automatically when you go back to the app.

On a related note when supporting light and dark mode - Use as many of the provided colors from UIColor as possible. See the available dynamic colors from UI Elements and Standard Colors. And when you need your own app-specific colors to support both light and dark mode, use the code in this answer as an example.


In Objective-C, you can define your own dynamic colors with:

UIColor+MyApp.h:

@interface UIColor (MyApp)

@property (class, nonatomic, readonly) UIColor *myControlBackgroundColor;

@end

UIColor+MyApp.m:

+ (UIColor *)myControlBackgroundColor {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traits) {
return traits.userInterfaceStyle == UIUserInterfaceStyleDark ?
[self colorWithRed:0.5 green:0.4 blue:0.2 alpha:1.0] :
[self colorWithRed:0.3 green:0.4 blue:0.5 alpha:1.0];
}];
} else {
return [self colorWithRed:0.3 green:0.4 blue:0.5 alpha:1.0];
}
}

Getting Dynamic UIColor from SwiftUI Color (light and dark mode)

I don't think what you want is possible. To achieve that you needed an initializer of Color that takes a light and dark variant and a way to resolve to a cgColor given a certain interface style (both not possible afaik). But your common ground could be the color name of your color from the asset catalogue and you could derive your SwiftUI/UIKit colors from the identifier.

struct ColorId: RawRepresentable, Equatable, Hashable {
let rawValue: String

init(_ rawValue: String) {
self.init(rawValue: rawValue)
}

init(rawValue: String) {
self.rawValue = rawValue
}

static let primaryText = ColorId("asset_name_of_primary_text_color")
}

extension ColorId {
var color: Color { Color(rawValue) }
var uiColor: UIColor? { UIColor(named: rawValue) }
}

struct SomeView: View {
let textColor = ColorId.primaryText
var body: some View {
Text("Text")
.foregroundColor(textColor.color)
}
}

final class SomeUIView: UILabel {
private let textColorId: ColorId

init(textColorId: ColorId) {
self.textColorId = textColorId
super.init(frame: .zero)
self.textColor = textColorId.uiColor
}
required init?(coder: NSCoder) { fatalError("need a color id") }
}

Then you pass around your color ids and get the UIColor or Color values when you need them. If you use something like RSwift you could also just pass around the ColorResource's.

How to access light and dark variants of React Material-UI primary and secondary colors?

When you create your custom theme pass it to ThemeProvider like this:

import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
...

const theme = createMuiTheme({
palette: {
secondary: {
main: '#287a9f'
}
}
});

function App() {

return (
<ThemeProvider theme={theme}>
<Children />
</ThemeProvider>
)
}

And there are many ways to access the theme variables, for example you could use makeStyles or useTheme:

makeStyles:

import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(theme => ({
myComp: { backgroundColor: theme.palette.primary.light }
});

function DeepChild() {
const classes = useStyles();

return <Component className={classes.myComp} />;
}

useTheme:

import { useTheme } from '@material-ui/core/styles';

function DeepChild() {
const theme = useTheme();

return <Component color={theme.palette.primary.light} />;
}

Note that the color property of the AppBar component supports one of

["default","inherit","primary","secondary","transparent"]

So in order to override the color for an AppBar component, as in your example, you need to pass a custom class:

import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';

const useStyles = makeStyles(theme => ({
appBar: { backgroundColor: theme.palette.primary.light }
});

function DeepChild() {
const classes = useStyles();

return <AppBar className={classes.appBar} />;
}

Get Slightly Lighter and Darker Color from UIColor

- (UIColor *)lighterColorForColor:(UIColor *)c
{
CGFloat r, g, b, a;
if ([c getRed:&r green:&g blue:&b alpha:&a])
return [UIColor colorWithRed:MIN(r + 0.2, 1.0)
green:MIN(g + 0.2, 1.0)
blue:MIN(b + 0.2, 1.0)
alpha:a];
return nil;
}

- (UIColor *)darkerColorForColor:(UIColor *)c
{
CGFloat r, g, b, a;
if ([c getRed:&r green:&g blue:&b alpha:&a])
return [UIColor colorWithRed:MAX(r - 0.2, 0.0)
green:MAX(g - 0.2, 0.0)
blue:MAX(b - 0.2, 0.0)
alpha:a];
return nil;
}

Use it like this:

UIColor *baseColor = // however you obtain your color
UIColor *lighterColor = [self lighterColorForColor:baseColor];
UIColor *darkerColor = [self darkerColorForColor:baseColor];

EDIT: as @Anchu Chimala pointed out, for maximum flexibility, these methods should be implemented as an UIColor category. Also, from @Riley's idea, it may be a better idea to make the color proprtionally darker or lighter instead of adding or subtracting constant values. As @jrturton pointed out, it's not necessary to manipulate the RGB components; it's better to modify the brightness property itself. All in all:

@implementation UIColor (LightAndDark)

- (UIColor *)lighterColor
{
CGFloat h, s, b, a;
if ([self getHue:&h saturation:&s brightness:&b alpha:&a])
return [UIColor colorWithHue:h
saturation:s
brightness:MIN(b * 1.3, 1.0)
alpha:a];
return nil;
}

- (UIColor *)darkerColor
{
CGFloat h, s, b, a;
if ([self getHue:&h saturation:&s brightness:&b alpha:&a])
return [UIColor colorWithHue:h
saturation:s
brightness:b * 0.75
alpha:a];
return nil;
}
@end

UIColor return wrong values for dark mode colors

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.

Detecting iOS Dark Mode Change

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



Related Topics



Leave a reply



Submit