```html SwiftUI: How to Implement Dark Mode (iOS 17+, 2026)

How to Implement Dark Mode in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: preferredColorScheme Updated: May 11, 2026
TL;DR

Attach .preferredColorScheme(_:) to your root view and drive it from an @AppStorage string — passing nil follows the device setting, while .dark or .light locks the appearance. Changes take effect instantly without restarting the app.

@main
struct MyApp: App {
    @AppStorage("appColorScheme") var scheme: String = "system"

    var resolvedScheme: ColorScheme? {
        switch scheme {
        case "dark":  return .dark
        case "light": return .light
        default:      return nil   // follow device setting
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .preferredColorScheme(resolvedScheme)
        }
    }
}

Full implementation

The pattern below stores the user's preference as a plain String in UserDefaults via @AppStorage, converts it to a typed AppColorScheme enum, and feeds the resolved ColorScheme? directly into .preferredColorScheme(_:) at the root. A segmented Picker in the UI lets the user switch modes live; because both the App struct and ContentView share the same @AppStorage key, the change propagates immediately across the hierarchy without any extra state plumbing.

import SwiftUI

// MARK: - Typed preference enum

enum AppColorScheme: String, CaseIterable, Identifiable {
    case system, light, dark

    var id: String { rawValue }

    var label: String {
        switch self {
        case .system: return "System"
        case .light:  return "Light"
        case .dark:   return "Dark"
        }
    }

    /// nil tells SwiftUI to follow the device setting
    var colorScheme: ColorScheme? {
        switch self {
        case .system: return nil
        case .light:  return .light
        case .dark:   return .dark
        }
    }
}

// MARK: - App entry point

@main
struct DarkModeApp: App {
    @AppStorage("appColorScheme") private var schemePref = AppColorScheme.system.rawValue

    private var resolved: ColorScheme? {
        AppColorScheme(rawValue: schemePref)?.colorScheme
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .preferredColorScheme(resolved)
        }
    }
}

// MARK: - Demo content view

struct ContentView: View {
    @AppStorage("appColorScheme") private var schemePref = AppColorScheme.system.rawValue
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        NavigationStack {
            VStack(spacing: 32) {
                Spacer()

                Image(systemName: colorScheme == .dark ? "moon.stars.fill" : "sun.max.fill")
                    .font(.system(size: 72))
                    .foregroundStyle(colorScheme == .dark ? .indigo : .orange)
                    .symbolEffect(.bounce, value: schemePref)
                    .accessibilityLabel(colorScheme == .dark ? "Dark mode active" : "Light mode active")

                Text("Appearance")
                    .font(.title2.bold())

                Text("Choose how \(Bundle.main.displayName ?? "this app") looks.")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)

                Picker("Color scheme", selection: $schemePref) {
                    ForEach(AppColorScheme.allCases) { scheme in
                        Text(scheme.label).tag(scheme.rawValue)
                    }
                }
                .pickerStyle(.segmented)
                .padding(.horizontal, 32)
                .accessibilityLabel("Select color scheme")

                Spacer()
            }
            .navigationTitle("Display")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

// MARK: - Bundle helper

private extension Bundle {
    var displayName: String? { object(forInfoDictionaryKey: "CFBundleDisplayName") as? String }
}

// MARK: - Previews

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

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

How it works

  1. AppColorScheme enum — wraps the three possible states (system, light, dark) in a type-safe way and exposes a colorScheme: ColorScheme? computed property. Returning nil for .system is the key detail: SwiftUI interprets a nil argument to .preferredColorScheme as "defer to the OS", which is exactly the System option's behaviour.
  2. @AppStorage("appColorScheme") — persists the raw string to UserDefaults so the preference survives cold launches. Both the App struct and ContentView declare the property with the same key, so writing from the Picker in the view automatically triggers a re-render of the root modifier.
  3. .preferredColorScheme(resolved) on WindowGroup's root — placing this modifier at the top of the hierarchy means every child view inherits the override, including sheets, full-screen covers, and navigation destinations. You don't need to repeat it in sub-views.
  4. @Environment(\.colorScheme) in ContentView — reads the effective scheme after the override is applied, which lets the view reactively swap the icon (moon.stars.fill vs sun.max.fill) and its tint colour. This value may differ from the device's own setting when the user has chosen Light or Dark explicitly.
  5. .symbolEffect(.bounce, value: schemePref) — triggers a subtle bounce animation on the SF Symbol whenever schemePref changes, giving the user tactile feedback that the switch worked. This API is available from iOS 17+ and requires no extra imports.

Variants

Per-view dark-mode override (without changing the whole app)

If you only want to force dark mode for a single sheet or modal — say, a video player — apply the modifier to that view alone instead of the root:

struct PlayerView: View {
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            VideoControlsOverlay()
        }
        // Forces this subtree to always render in dark,
        // regardless of the user's app-wide preference.
        .preferredColorScheme(.dark)
    }
}

#Preview {
    PlayerView()
}

Programmatic toggle (no Picker UI)

If you want a single tap-to-toggle button rather than a three-option picker, maintain a Bool via @AppStorage("isDarkMode") and map it directly: .preferredColorScheme(isDarkMode ? .dark : .light). This drops the "follow system" option but is simpler for minimal UIs. Pair it with a Toggle bound to the same key for a Settings-style row.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement dark mode in SwiftUI for iOS 17+.
Use preferredColorScheme and @AppStorage to persist the preference.
Expose a segmented Picker with System / Light / Dark options.
Make it accessible (VoiceOver labels on the icon and Picker).
Add a #Preview with both .light and .dark variants showing realistic content.

In the Soarias Build phase, drop this prompt into the active session after your screens are scaffolded — the agent will wire up the AppColorScheme enum, the @AppStorage key, and the root modifier in one pass, leaving you a clean diff to review before committing.

Related

FAQ

Does this work on iOS 16?

Yes — .preferredColorScheme(_:) and @AppStorage are both available from iOS 14+. The only iOS 17-specific API used in the full example is .symbolEffect(.bounce); wrap it in if #available(iOS 17, *) { } or remove it entirely and everything else compiles and runs on iOS 16 unchanged.

How do I apply a dark mode override inside a SwiftUI sheet or fullScreenCover?

Sheets are presented in a new view hierarchy layer, so they inherit the .preferredColorScheme modifier only when it is placed on the WindowGroup root — not on the view that triggers the sheet. If you see a sheet reverting to the system scheme, double-check that your modifier lives in the App struct (as shown in the full implementation), not inside a child view.

What is the UIKit equivalent?

In UIKit you set window.overrideUserInterfaceStyle to .dark, .light, or .unspecified (the equivalent of nil in SwiftUI). For individual view controllers use viewController.overrideUserInterfaceStyle. The SwiftUI .preferredColorScheme modifier maps directly to this UIKit property under the hood.

Last reviewed: 2026-05-11 by the Soarias team.

```