Reputation: 2764
I've created a custom sheet in SwiftUI with the background color White .background(Color.white)
Now I want the background color to change to black when the user turns on the dark mode on iOS.
But I can't find a dynamic color for background like Color.primary
for colors of the text etc.
So is there any way to change the background color to black when dark mode turns on?
Upvotes: 74
Views: 80499
Reputation: 11426
usage of dynamic color init:
let textColor = Color(light: someColor, dark: anotherColor)
Source code taken from following blog:
https://www.jessesquires.com/blog/2023/07/11/creating-dynamic-colors-in-swiftui/
import SwiftUI
#if canImport(AppKit)
import AppKit
#endif
#if canImport(UIKit)
import UIKit
#endif
@available(macOS 12.0, *)
extension Color {
init(light: Color, dark: Color) {
#if canImport(UIKit)
self.init(light: UIColor(light), dark: UIColor(dark))
#else
self.init(light: NSColor(light), dark: NSColor(dark))
#endif
}
#if canImport(UIKit)
init(light: UIColor, dark: UIColor) {
#if os(watchOS)
// watchOS does not support light mode / dark mode
// Per Apple HIG, prefer dark-style interfaces
self.init(uiColor: dark)
#else
self.init(uiColor: UIColor(dynamicProvider: { traits in
switch traits.userInterfaceStyle {
case .light, .unspecified:
return light
case .dark:
return dark
@unknown default:
assertionFailure("Unknown userInterfaceStyle: \(traits.userInterfaceStyle)")
return light
}
}))
#endif
}
#endif
#if canImport(AppKit)
init(light: NSColor, dark: NSColor) {
self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
switch appearance.name {
case .aqua,
.vibrantLight,
.accessibilityHighContrastAqua,
.accessibilityHighContrastVibrantLight:
return light
case .darkAqua,
.vibrantDark,
.accessibilityHighContrastDarkAqua,
.accessibilityHighContrastVibrantDark:
return dark
default:
assertionFailure("Unknown appearance: \(appearance.name)")
return light
}
}))
}
#endif
}
Upvotes: 0
Reputation: 2762
The colorset answer is best choice for global colors in your app, but if you want to quickly set a color in code and not touch your xcassets file this is the best way:
create a color extension:
extension Color {
func darkMode(_ darkColor: Color, _ colorScheme: ColorScheme) -> Color {
return colorScheme == .dark ? darkColor : self
}
}
then use it in your view. You will have to pass in a colorScheme. I wish is could set this in the extension but unfortunatly this has to be set in the view and passed in.
struct SampleColor: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text("Hello, World!!")
.padding()
.background(Color.green.darkMode(Color(red: 0, green: 29, blue: 33), colorScheme))
.cornerRadius(10)
.foregroundColor(.white.darkMode(.black, colorScheme))
}
}
Then preview your view
struct SampleColor_Previews: PreviewProvider {
static var previews: some View {
Group {
SampleColor()
.previewDisplayName("Light Mode")
SampleColor()
.previewDisplayName("Dark Mode")
.preferredColorScheme(.dark)
}
}
}
Upvotes: 1
Reputation: 257533
Here is possible approach (for any color)
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
...
var body: some View {
// ... to any view
.background(colorScheme == .dark ? Color.black : Color.white)
}
}
Upvotes: 16
Reputation: 320
I built a bridge between UIColor and Color:
fileprivate extension UIColor {
static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
UIColor {
switch $0.userInterfaceStyle {
case .dark:
return dark
default:
return light
}
}
}
}
extension Color {
static var itemTextColor: Color = Color(uiColor: .dynamicColor(
light: UIColor(red:0.278, green:0.278, blue:0.278, alpha: 1.0),
dark: UIColor(red:0.800, green:0.800, blue:0.800, alpha:1.000))
)
}
Upvotes: 1
Reputation: 573
Here is my solution. Inspired by Luc-Oliver's solution 2.
extension ColorScheme {
typealias ColorSelector = (Color, Color) -> Color
private var colorSelector: ColorSelector { color(_:_:) }
func color(_ light: Color, _ dark: Color) -> Color {
switch self {
case .light: return light
case .dark: return dark
@unknown default: fatalError()
}
}
struct Provider<Content>: View where Content: View
{
typealias ColorSelector = ColorScheme.ColorSelector
typealias ContentGetter = (ColorSelector) -> Content
@Environment(\.colorScheme) var colorScheme
let content: ContentGetter
init(@ViewBuilder content: @escaping ContentGetter) {
self.content = content
}
var body: some View {
content(colorScheme.colorSelector)
}
}
}
Use like:
struct DemoView: View {
var body: some View {
ColorScheme.Provider { color in
HStack {
Rectangle().fill(color(.blue, .red))
Rectangle().fill(color(.green, .orange))
}
}
}
}
Upvotes: 0
Reputation: 3973
Edit #2
Here are 2 completes solutions:
Both are updated on the fly when Mode status changes.
Both work in Preview and on true device.
Both manage a Color Theme based on Mode status (inspired from JetPack Compose).
In solution 1, OSModeTheme & OSModeThemeUpdater work together to provide the right mode status and color theme as static values, offering the possibility to define colors at the top level, eg: in default values of a view init parameters.
init(color: Color = OSModeTheme.colors.primary) { ... }
In solution 2, OSModeThemeProvider is a View Wrapper providing a local variable containing the right ColorTheme according to the Mode status.
OSModeThemeProvider { colors in
Text("Foo bar")
.foregroundColor(colors.primary)
}
// Commun part
protocol Palette {
static var primary: Color { get }
static var primaryVariant: Color { get }
static var secondary: Color { get }
static var secondaryVariant: Color { get }
static var accentColor: Color { get }
static var background: Color { get }
static var frame: Color { get }
static var error: Color { get }
}
struct LightColorPalette: Palette {
static var primary = ColorPalette.black
static var primaryVariant = ColorPalette.grayDark
static var secondary = ColorPalette.grayMid
static var secondaryVariant = ColorPalette.grayLight
static var accentColor = ColorPalette.blue
static var background = ColorPalette.white
static var frame = ColorPalette.grayDark
static var error = ColorPalette.orange
}
struct DarkColorPalette: Palette {
static var primary = ColorPalette.white
static var primaryVariant = ColorPalette.grayLight
static var secondary = ColorPalette.grayLight
static var secondaryVariant = ColorPalette.grayMid
static var accentColor = ColorPalette.blue
static var background = ColorPalette.black
static var frame = ColorPalette.grayLight
static var error = ColorPalette.orange
}
// Solution 1
class OSModeTheme {
static var colorScheme: ColorScheme = .light
static var colors: Palette.Type = LightColorPalette.self
static func update(mode: ColorScheme) {
colorScheme = mode
colors = colorScheme == .dark ? DarkColorPalette.self : LightColorPalette.self
}
}
struct OSModeThemeUpdater<Content>: View where Content: View {
@Environment(\.colorScheme) var colorScheme
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
OSModeTheme.update(mode: colorScheme)
return content()
}
}
struct OSModeThemeDemo: View {
var body: some View {
OSModeThemeUpdater {
ZStack {
Rectangle()
.fill(OSModeTheme.colors.background)
VStack {
Group {
Text("primary")
.foregroundColor(OSModeTheme.colors.primary)
Text("primaryVariant")
.foregroundColor(OSModeTheme.colors.primaryVariant)
Text("Secondary")
.foregroundColor(OSModeTheme.colors.secondary)
Text("secondaryVariant")
.foregroundColor(OSModeTheme.colors.secondaryVariant)
Text("accentColor")
.foregroundColor(OSModeTheme.colors.accentColor)
Text("background")
.foregroundColor(OSModeTheme.colors.background)
Text("frame")
.foregroundColor(OSModeTheme.colors.frame)
Text("error")
.foregroundColor(OSModeTheme.colors.error)
}
}
}
}
}
}
// Solution 2
struct OSModeThemeProvider<Content>: View where Content: View {
@Environment(\.colorScheme) var colorScheme
let content: (Palette.Type) -> Content
init(@ViewBuilder content: @escaping (Palette.Type) -> Content) {
self.content = content
}
var body: some View {
content(colorScheme == .dark ? DarkColorPalette.self : LightColorPalette.self)
}
}
struct OSModeThemeProviderDemo: View {
var body: some View {
OSModeThemeProvider { palette in
ZStack {
Rectangle()
.fill(palette.background)
VStack {
Text("primary")
.foregroundColor(palette.primary)
Text("primaryVariant")
.foregroundColor(palette.primaryVariant)
Text("Secondary")
.foregroundColor(palette.secondary)
Text("secondaryVariant")
.foregroundColor(palette.secondaryVariant)
Text("accentColor")
.foregroundColor(palette.accentColor)
Text("background")
.foregroundColor(palette.background)
Text("frame")
.foregroundColor(palette.frame)
Text("error")
.foregroundColor(palette.error)
}
}
}
}
}
Upvotes: 5
Reputation: 18680
Check out this page for recommended system colors for various UI elements. Using these should take care of dark/light mode switching.
Upvotes: 2
Reputation: 14955
SwiftUI has colors that adapt automatically to the color scheme. For example, you can use .background(Color(.textBackgroundColor))
to get an appropriate background color for rendering text regardless of color scheme.
Upvotes: 0
Reputation: 3342
Personally I don't like to create a color set in the Assets
folder.
I prefer it to be in the code so the best practices for this are as follows:
extension Color {
static var primaryColor: Color {
Color(UIColor { $0.userInterfaceStyle == .dark ? UIColor(red: 255, green: 255, blue: 255, alpha: 1) : UIColor(red: 200, green: 200, blue: 200, alpha: 1) })
}
}
Using:
.background(Color.primaryColor)
Upvotes: 11
Reputation: 1094
We can also change the color automatically by adding them to the Assets
folder.
After you add a color set, you can name it as per your convenience and you can configure your color for Any Appearance
, Dark Appearance
, Light Appearance
.
To access your newly added color set, you need to follow the following initializer syntax of Color
Color("your_color_set_name")
For best practice you would not want your code filled with string values of your Color set name. You can create an extension to make usage more pragmatic and ordered.
extension Color {
static var tableViewBackground: Color {
Color("tableViewBackground")
}
}
Upvotes: 18
Reputation: 991
If you wish to use custom background colour for light/dark mode then I would recommend creating New Colour set in your Assets folder with custom colour values for different Appearances.
That way background colour will change automatically when display mode is switched without the need of adding a single line of code.
And then using this color of the colour list for Controller View background.
Upvotes: 8
Reputation: 3742
If you want something that works directly from Color
(like you're doing with Color.primary
), and functions on both iOS and macOS (UIColor
won't work on macOS), you can use the following simple Color
extension, which uses conditional compilation to work correctly on either OS.
You then simply access these from elsewhere in your code like any other SwiftUI Color
. For example:
let backgroundColor = Color.background
No need to check colorScheme
or userInterfaceStyle
with this approach: The OS will switch automatically when the user moves between Light & Dark mode.
I've also included 'secondary' & 'tertiary' colors, which are a little subjective on macOS, but you can always change them to some of the other NSColor
properties if you want.
Swift v5.2:
import SwiftUI
public extension Color {
#if os(macOS)
static let background = Color(NSColor.windowBackgroundColor)
static let secondaryBackground = Color(NSColor.underPageBackgroundColor)
static let tertiaryBackground = Color(NSColor.controlBackgroundColor)
#else
static let background = Color(UIColor.systemBackground)
static let secondaryBackground = Color(UIColor.secondarySystemBackground)
static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
#endif
}
Upvotes: 30
Reputation: 349
You can extend UIColor as shown below
extension UIColor{
struct Custom {
static var black: UIColor{
if #available(iOS 13, *) {
return UIColor.init { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
}
}
return UIColor.black
}
}
}
Then use as .background(Color(UIColor.Custom.black))
Your view will update the color when dark move is enabled/disabled
Upvotes: 1
Reputation: 2225
To elaborate on the two existing answers, there are a couple of approaches to making the background change based on light or dark mode (aka colorScheme
) depending on what you're trying to achieve.
If you set the background color to white because that's the default background color, and you want the system to be able to update it when the user switches to dark mode, change .background(Color.white)
to .background(Color(UIColor.systemBackground))
(umayanga's answer).
e.g.
// Use default background color based on light/dark mode
struct ContentView: View {
...
var body: some View {
// ... to any view
.background(Color(UIColor.systemBackground))
}
If you want to customize the color of a view based on the device being in light or dark mode, you can do this (from Asperi's answer):
// Use custom background color based on light/dark mode
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
...
var body: some View {
// ... to any view
.background(colorScheme == .dark ? Color.black : Color.white)
}
Note that many SwiftUI views set their background color to .systemBackground
by default, so if you're using a ScrollView, List, Form, etc, they'll use the default system background color and you won't need to use .background
unless you want to customize it.
Upvotes: 123
Reputation: 2764
Change the .background(Color.white)
to .background(Color(UIColor.systemBackground))
Upvotes: 21