Beginning with the introduction of dark mode in iOS 13, colors in iOS are now (optionally) dynamic. You can provide light and dark variants for all colors in your app. However, I was surprised to find that SwiftUI — which also made its first appearance on the platform in iOS 13 — still does not provide any API for creating dynamic colors.

In UIKit, UIColor provides a dynamic initializer, init(dynamicProvider:), which I wrote about here. AppKit provides the equivalent API for NSColor. Unfortunately, an equivalent API for SwiftUI’s Color is missing.

UIKit also allows you to extract a specific variant from a UIColor using resolvedColor(with:), which will return either the dark or light variant based on the provided trait collection. Again, AppKit provides the equivalent API for NSColor. Surprisingly, in iOS 17 SwiftUI’s Color gained a new API for color resolution, resolve(in:), which returns the resolved color value based on the provided EnvironmentValues.

The result is that SwiftUI’s Color API is oddly incomplete. Color has no equivalent API to UIColor.init(dynamicProvider:), but it does provide its own version of UIColor.resolvedColor(with:). This is not only inconvenient, but very confusing.

Of course, you can use Asset Catalogs to define dynamic colors and reference them in SwiftUI, and Xcode 15 makes that easier! But if you need to programmatically initialize dynamic colors in SwiftUI, you are out of luck due to this glaring omission. Instead, you must resort to UIKit and AppKit. So, here’s a helpful extension that accommodates the missing API for all platforms.

import SwiftUI

#if canImport(AppKit)
import AppKit
#endif

#if canImport(UIKit)
import UIKit
#endif

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
}

And now you can initialize a SwiftUI Color programmatically with a light and dark variant.

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