Add a dark mode preference to a SwiftUI app

This article is a compact walkthrough for one specific SwiftUI pattern: let the user choose between following the system appearance, always using dark mode, or always using light mode, then keep that choice in @AppStorage and apply it from the app root.

SwiftUI app switching between follow system, dark mode, and light mode

This is a small feature, but the clean version matters: store an integer setting once, bind UI to it, and map it to a color scheme in one place.

This article is short and focused. It does not try to build a full settings architecture. It just shows the simplest way to let a SwiftUI app remember whether the user wants to follow the system appearance, force dark mode, or force light mode.

The implementation is split into three pieces: define the setting values, save the selected value with @AppStorage, then apply the matching preferredColorScheme at the app root.

Project Link This article links to the sample project at SwiftUI_Dark-Mode-Toggle.

The stored value is just an enum backed by an integer plus one @AppStorage property named appearanceMode.

The article starts by defining the three states explicitly:

enum DarkModeSetting: Int {
    case followSystem = 0
    case darkMode = 1
    case lightMode = 2
}

Then it persists the selected value through @AppStorage:

@AppStorage(wrappedValue: 0, "appearanceMode")
var appearanceMode

That gives the app a simple preference store without introducing a separate settings object or manual UserDefaults plumbing.

Dark mode preference switching inside a SwiftUI app
The article's demo shows the app switching among system, dark, and light appearance modes.

Once the value is in @AppStorage, the settings UI is only a bound picker with three tagged rows.

This article adds the preference directly in a settings-style screen:

Picker("Appearance setting", selection: $appearanceMode) {
    Text("Follow system")
        .tag(0)
    Text("Dark mode")
        .tag(1)
    Text("Light mode")
        .tag(2)
}

Because the picker is bound to @AppStorage, changing the selection updates the saved value automatically. There is no extra save button and no manual synchronization step.

The important part is to read the stored mode from the App entry point and map it to the right preferredColorScheme.

The article applies the appearance choice in the main app structure, not only inside one screen:

@main
struct SwiftUI_Toggle_DarkModeApp: App {

    @AppStorage(wrappedValue: 0, "appearanceMode")
    var appearanceMode

    var body: some Scene {
        WindowGroup {
            ContentView()
                .applyAppearenceSetting(
                    DarkModeSetting(rawValue: self.appearanceMode) ?? .followSystem
                )
        }
    }
}

The helper function is just a switch that maps the stored enum to .none, .dark, or .light:

extension View {
    @ViewBuilder
    func applyAppearenceSetting(_ setting: DarkModeSetting) -> some View {
        switch setting {
        case .followSystem:
            self.preferredColorScheme(.none)
        case .darkMode:
            self.preferredColorScheme(.dark)
        case .lightMode:
            self.preferredColorScheme(.light)
        }
    }
}

The naming in the source uses applyAppearenceSetting. You can keep that spelling to match the sample or rename it locally if you want cleaner naming.

There is one caveat in this article: changing the preferred color scheme can send the app back to its default screen.

That note matters because this setting is applied high in the view tree. If the current navigation state is not being preserved independently, a full appearance change can feel like a lightweight app reset from the user's perspective.

For a small app that may be fine. For a larger app, it is a reminder to test how theme changes interact with navigation, presented sheets, and in-progress form state.

This pattern stays useful because it solves appearance preference end to end without much code: one stored integer, one picker, one app-level mapping.

If you want a SwiftUI app to respect a user-selected appearance override instead of only following the system theme, this is still a clean baseline implementation.