Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
How-to

Using ColorTokensKit in UIKit

Give UIKit views the palette's tokens as UIColors that follow dark mode on every iOS version tested.

Updated View as Markdown

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.

swift
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.uiForegroundSecondary
light mode
3 nights from October 12

UIColor(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#

swift
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 dark
light mode
3 nights from October 12

Color 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#

swift
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))
light mode

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#

swift
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#

See also