umayanga
umayanga

Reputation: 2764

Change background color when dark mode turns on in SwiftUI

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

Answers (15)

Andrew_STOP_RU_WAR_IN_UA
Andrew_STOP_RU_WAR_IN_UA

Reputation: 11426

2023

usage of dynamic color init:

let textColor = Color(light: someColor, dark: anotherColor)

Code works on IOS and MacOS both

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

Richard Torcato
Richard Torcato

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

Asperi
Asperi

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

Roger Lee
Roger Lee

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

SirEnder
SirEnder

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

Luc-Olivier
Luc-Olivier

Reputation: 3973

Edit #2

Here are 2 completes solutions:

  1. Giving a control of mode status (Light or Dark) at Application Level
  2. Giving a control of mode status at View Level

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

Nelu
Nelu

Reputation: 18680

Check out this page for recommended system colors for various UI elements. Using these should take care of dark/light mode switching.

UI Element Colors

Upvotes: 2

JuJoDi
JuJoDi

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

Mickael Belhassen
Mickael Belhassen

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

G.Abhisek
G.Abhisek

Reputation: 1094

We can also change the color automatically by adding them to the Assets folder.

  1. Add a new color set in the Assets folder enter image description here

  2. 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. enter image description here

  3. To access your newly added color set, you need to follow the following initializer syntax of Color

    Color("your_color_set_name")
    
    
  4. 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

marika.daboja
marika.daboja

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.

enter image description here

And then using this color of the colour list for Controller View background.

enter image description here

Upvotes: 8

TheNeil
TheNeil

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

Moin Uddin
Moin Uddin

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

ggruen
ggruen

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

umayanga
umayanga

Reputation: 2764

Change the .background(Color.white) to .background(Color(UIColor.systemBackground))

Upvotes: 21

Related Questions