BS
BleepingSwift
Published on

> Supporting Dark Mode in a SwiftUI App

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

SwiftUI supports dark mode automatically if you stick to system colors and standard components. The challenge comes when you need custom brand colors—that's when apps start looking washed out or unreadable in one mode or the other.

Why It Works by Default

A basic SwiftUI list adapts without any work because the default colors are semantic. Instead of specifying "black text on white background," SwiftUI uses concepts like "primary text on system background." The system decides what those actually mean based on the current appearance.

Text("This adapts automatically")
    .foregroundStyle(.primary)

The .primary color is nearly black in light mode and nearly white in dark mode. Same with .secondary for less prominent text. For backgrounds, use Color(.systemBackground) and its variants—these come from UIKit but work perfectly in SwiftUI.

Rectangle()
    .fill(Color(.systemBackground))

The trap is using .white or .black directly. These are absolute colors that never change, so white text on a custom blue background looks fine until someone in dark mode sees white text on a nearly-white system background.

Custom Colors That Adapt

When you need specific brand colors, create them in your asset catalog with both light and dark variants. Add a new Color Set, change its Appearances to "Any, Dark" in the inspector, then set different values for each. Reference it by name:

Text("Brand colored")
    .foregroundStyle(Color("BrandAccent"))

If you prefer keeping colors in code, create them using UIColor's trait collection initializer:

extension Color {
    static let cardBackground = Color(
        UIColor { traits in
            traits.userInterfaceStyle == .dark
                ? UIColor(white: 0.15, alpha: 1)
                : UIColor(white: 0.97, alpha: 1)
        }
    )
}

Detecting the Current Mode

Sometimes you need to know which mode is active—maybe to swap images or adjust a layout. Read the colorScheme environment value:

struct LogoView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        Image(colorScheme == .dark ? "logo-light" : "logo-dark")
    }
}

You can also force a specific appearance on part of your view hierarchy:

ContentView()
    .preferredColorScheme(.dark)

Pass nil to follow the system setting, which is useful if you're letting users choose in your app's settings.

Testing Both Modes

Add two previews to see both appearances side by side:

#Preview("Light") {
    ContentView()
        .preferredColorScheme(.light)
}

#Preview("Dark") {
    ContentView()
        .preferredColorScheme(.dark)
}

In the simulator, toggle appearance in the Environment Overrides panel from Xcode's debug bar, or through Settings on the device.

The main thing to remember: start with semantic colors like .primary and Color(.systemBackground). They handle dark mode correctly and respect accessibility settings. Only reach for custom colors when your design requires them, and always define both variants.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.