Apr 28, 2026

Appearance Mode Changer

Appearance Mode Changer

Creating an Appearance Mode Picker in SwiftUI

Many apps give users a simple choice for how the interface should look:

  • Follow the system appearance
  • Always use light mode
  • Always use dark mode

SwiftUI makes this surprisingly easy. The trick is to model the choice once, save it with @AppStorage, and apply it at the root of your app with preferredColorScheme.

In this post, we will build an appearance mode setting that can be reused in almost any SwiftUI application.

The Goal

We want the user to choose an appearance mode from inside the app. Once selected, the app should update immediately and remember that choice the next time it launches.

The finished behavior looks like this:

  • System follows the device's current light or dark mode setting.
  • Light forces the app to use light mode.
  • Dark forces the app to use dark mode.
  • The selected value persists across launches.
  • Navigation views and modal sheets inherit the selected appearance.

Create the Appearance Mode Type

Start by creating an enum to represent the three supported modes.

import SwiftUI

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

    var id: Self { self }

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

    var iconName: String {
        switch self {
        case .system:
            "sun.righthalf.filled"
        case .light:
            "sun.max.fill"
        case .dark:
            "moon.fill"
        }
    }

    var colorScheme: ColorScheme? {
        switch self {
        case .system:
            nil
        case .light:
            .light
        case .dark:
            .dark
        }
    }
}

There are a few useful details here.

CaseIterable lets us build a picker by looping over all cases. Identifiable makes the enum convenient to use in ForEach. The title and iconName properties keep display details close to the option they describe.

The most important property is colorScheme.

var colorScheme: ColorScheme? {
    switch self {
    case .system:
        nil
    case .light:
        .light
    case .dark:
        .dark
    }
}

preferredColorScheme accepts an optional ColorScheme. Passing .light or .dark forces that appearance. Passing nil tells SwiftUI not to override the system appearance.

That makes nil the perfect value for the System option.

Save the Selection with AppStorage

Because AppearanceMode is backed by a String raw value, it works well with @AppStorage.

@AppStorage("appearanceMode") private var appearanceMode = AppearanceMode.system

This stores the user's choice in UserDefaults. When the value changes, SwiftUI updates any view that depends on it. When the app launches again, the saved value is restored automatically.

You can use any key name you like, but keep it stable once your app ships. Changing the key later would make existing users lose their saved appearance preference.

Apply the Appearance at the App Root

The appearance mode should be applied as high in the view hierarchy as possible. In most apps, that means the App entry point.

import SwiftUI

@main
struct MyApp: App {
    @AppStorage("appearanceMode") private var appearanceMode = AppearanceMode.system

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

Applying preferredColorScheme here means the choice affects the whole app, including pushed navigation destinations and presented sheets.

If you apply the modifier deeper in the hierarchy, you may end up with only part of the interface changing appearance. That can be useful for special cases, but for a user-facing app setting, the root is usually the right place.

Add a Picker to the Interface

Now you need a way for the user to change the value.

Here is a simple toolbar menu:

struct ContentView: View {
    @AppStorage("appearanceMode") private var appearanceMode = AppearanceMode.system

    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)

                Text("Hello, world!")
            }
            .padding()
            .navigationTitle("Mode Changer")
            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Menu {
                        Picker("Appearance", selection: $appearanceMode) {
                            ForEach(AppearanceMode.allCases) { mode in
                                Label(mode.title, systemImage: mode.iconName)
                                    .tag(mode)
                            }
                        }
                    } label: {
                        Image(systemName: appearanceMode.iconName)
                    }
                }
            }
        }
    }
}

The picker writes directly to the same @AppStorage value. As soon as the selection changes, the app-level preferredColorScheme receives the new value and the interface updates.

Why Use a Menu?

A menu works well when the setting is available from a toolbar. It keeps the interface tidy while still making the choice easy to reach.

For a settings screen, you may prefer a segmented picker or a list row:

Picker("Appearance", selection: $appearanceMode) {
    ForEach(AppearanceMode.allCases) { mode in
        Label(mode.title, systemImage: mode.iconName)
            .tag(mode)
    }
}

The important part is not the picker style. The important part is that every picker binds to the same stored appearanceMode value.

Make Sure Sheets and Navigation Inherit the Mode

Because the color scheme is applied at the app root, child views do not need to know anything about the appearance setting.

struct NavigationStackView: View {
    var body: some View {
        VStack {
            Text("This view was pushed onto the navigation stack.")
            Image(systemName: "star.fill")
        }
        .font(.largeTitle)
        .padding()
        .navigationTitle("Pushed View")
    }
}

Presented sheets also inherit the selected mode:

struct ModalSheetView: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            VStack {
                Text("Presented as a modal sheet.")
                Image(systemName: "apple.logo")
            }
            .font(.largeTitle)
            .padding()
            .toolbar {
                Button(role: .close) {
                    dismiss()
                }
            }
        }
    }
}

These views stay clean because appearance mode is an app-level concern, not a screen-level concern.

The Complete Pattern

The reusable pattern is:

  1. Create an AppearanceMode enum.
  2. Store the selected value with @AppStorage.
  3. Apply preferredColorScheme at the App root.
  4. Bind any picker or settings control to the same stored value.

Once those pieces are in place, you can expose the setting anywhere in your app.

Final Thoughts

Appearance mode is a small feature, but it is one of those details that makes an app feel more considerate. Some users want their apps to follow the system. Others want a specific app to stay light or dark regardless of the device setting.

With SwiftUI, supporting all three options only takes a small enum, a stored preference, and one app-wide modifier.

That is a tidy little feature with a lot of user-facing polish.

If you want to download the code for a sample project using this technique you can find on my GitHub.

ModeChanger