Your UIKit views get the same palette and tokens as SwiftUI, with dark mode handled. Build each token as a dynamic UIColor from its two stops: it switches with dark mode on every iOS version we tested, which converting the SwiftUI token doesn't always do.
import UIKit
import SwiftUI
import ColorTokensKit
extension ProTheme {
/// foregroundSecondary for UIKit: _800 in light mode, _200 in dark mode.
var uiForegroundSecondary: UIColor {
UIColor(light: UIColor(_800.toColor()), dark: UIColor(_200.toColor()))
}
}
let dates = UILabel()
dates.text = "3 nights from October 12"
dates.font = .preferredFont(forTextStyle: .subheadline)
dates.textColor = Color.proOrange.uiForegroundSecondaryUIColor(light:dark:) comes with the package. UIColor takes a SwiftUI Color, not a stop, so each stop goes through toColor(). Understanding semantic tokens lists every token's stops.
Palette colors travel as extended sRGB, so a Display P3 stop, such as orange _200 here, keeps its full color in a UIColor. The same code builds for tvOS and visionOS.
Adapt color function results#
import UIKit
extension UIColor {
/// Works out a SwiftUI color for each appearance.
@available(iOS 17.0, tvOS 17.0, *)
convenience init(adapting color: Color) {
self.init { traits in
var environment = EnvironmentValues()
environment.colorScheme = traits.userInterfaceStyle == .dark ? .dark : .light
let resolved = color.resolve(in: environment)
return UIColor(
red: CGFloat(resolved.red),
green: CGFloat(resolved.green),
blue: CGFloat(resolved.blue),
alpha: CGFloat(resolved.opacity)
)
}
}
}
let dates = UILabel()
dates.text = "3 nights from October 12"
dates.font = .preferredFont(forTextStyle: .subheadline)
dates.textColor = UIColor(adapting: Color.proOrange.foregroundSecondary.soften()) // _750 in light mode, _250 in darkColor functions such as soften(by:) work out their result for each mode, and UIColor(adapting:) keeps that in UIKit on iOS 17 and later. On iOS 16, build the result from those stops with UIColor(light:dark:).
Update layer colors when the mode changes#
import UIKit
final class TripCardView: UIView {
// outlineSecondary: gray _200 in light mode, _800 in dark mode
private let outline = UIColor(
light: UIColor(Color.proGray._200.toColor()),
dark: UIColor(Color.proGray._800.toColor())
)
override init(frame: CGRect) {
super.init(frame: frame)
layer.borderWidth = 1
updateBorder()
registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: TripCardView, _: UITraitCollection) in
view.updateBorder()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateBorder() {
layer.borderColor = outline.resolvedColor(with: traitCollection).cgColor
}
}
let tripCard = TripCardView(frame: CGRect(x: 0, y: 0, width: 120, height: 48))A CGColor holds one value, so set layer.borderColor again when the mode changes. On iOS 16, call updateBorder() from traitCollectionDidChange(_:) instead.
If something goes wrong#
A UIKit color stays the same in dark mode#
Either a layer still holds a CGColor from the other mode, or the color came from UIColor(_:) on an adaptive color. In our tests, that kept only the light value on iOS 17 and 18.0, even on a real view, as Apple's Color.primary did, and followed the mode on 18.6 and 26. Build the color with UIColor(light:dark:) or UIColor(adapting:), and set layer colors again when the mode changes.
API#
extension UIColor {
convenience init(
light lightModeColor: @escaping @autoclosure () -> UIColor,
dark darkModeColor: @escaping @autoclosure () -> UIColor
)
}A dynamic UIColor that gives light in the light appearance and dark in the dark one; an unspecified appearance gets light. It's available on iOS, iPadOS, tvOS and visionOS, and not on watchOS.
Sources#
- The library's UIColor+Dynamic.swift, which defines
UIColor(light:dark:)for every UIKit platform except watchOS. - Apple, init(dynamicProvider:), the dynamic
UIColorit builds on. - Apple, registerForTraitChanges(_:handler:), for updating layer colors on iOS 17 and later.
See also
- Managing dark mode Get dark mode without extra code: tokens switch stops for you, and your own colors can too.
- Understanding semantic tokens Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.
- Using ColorTokensKit in AppKit Give AppKit views the palette's tokens as NSColors that follow the Mac's light and dark appearance.
- Using ColorTokensKit on visionOS Use the palette on Apple Vision Pro, where tokens give their dark stops and color goes on buttons.
- High contrast modes Respect Increase Contrast with one rule for every family: text and borders two stops stronger.