Reading and setting color scheme in SwiftUI

Users can set a preferred color scheme to light or dark on their devices and when our apps use standard system colors in SwiftUI, our interface adapts automatically.

In some cases, however, we need to read the current color scheme to adjust our app's appearance or even manually set a color scheme to implement a custom design.

# Reading current color scheme

Reading the current color scheme in SwiftUI is quite straight forward, it's provided via colorScheme environment value. We can read the currently set value in the environment from any SwiftUI view and adapt our interface.

In the example below we set a lighter color for the button in dark appearance and darker color in light appearance to increase contrast.

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        Button(action: {
            // do something
        }) {
            Image(systemName: "xmark.circle.fill")
        }
        .foregroundColor(
            colorScheme == .dark ? .lightGray : .darkGray
        )
    }
}

extension Color {
    static let lightGray = Color(
        uiColor: UIColor.lightGray
    )
    static let darkGray = Color(
        uiColor: UIColor.darkGray
    )
}

# Setting color scheme

# Setting color scheme preference

We can set a color scheme for the entire app using preferredColorScheme(_:) modifier.

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .preferredColorScheme(.dark)
    }
}

This modifier sets a preference that floats up the view hierarchy. Applying it on a child view will still affect the whole app, with some exceptions that we'll see further.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Text 1")
                .preferredColorScheme(.dark)
            Text("Text 2")
        }
    }
}

Preference set higher in the hierarchy overrides the preference set lower in the hierarchy. In the following example the entire app will have the light color scheme.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Text 1")
                .preferredColorScheme(.dark)
            Text("Text 2")
        }
        .preferredColorScheme(.light)
    }
}

It's important to note, however, that the color scheme preference is intercepted on the presentation level. This means that setting it inside a sheet, popover or full screen cover, will only affect the color scheme of that presentation.

In the example below the sheet presentation will have dark color scheme, but the rest of the app will follow the color scheme of the device.

struct ContentView: View {
    @State private var showSheet = false
    
    var body: some View {
        VStack {
            Button("Show sheet") {
                showSheet = true
            }
            .sheet(isPresented: $showSheet) {
                Text("Sheet content")
                    .preferredColorScheme(.dark)
            }
        }
    }
}

# Setting color scheme environment value

In some advanced use cases we might want to change the color scheme of a portion of the hierarchy or a specific view. For such cases we can override the colorScheme environment value.

In our example MyCustomView will have dark color scheme, which will make its text always appear in light color, independent of the device color scheme.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Text 1")
            MyCustomView()
                .environment(\.colorScheme, .dark)
                .background(.purple)
        }
    }
}

struct MyCustomView: View {
    var body: some View {
        Text("Text 2")
            .padding()
    }
}

Note, that there is also the old modifier colorScheme(_:), which has been deprecated. It works the same way as colorScheme environment value and sets the environment value under the hood. The documentation recommends using preferredColorScheme(_:) modifier instead. The idea is that we most likely want to set the color scheme on the whole presentation/window, so should use preferredColorScheme(_:) in most cases. In rare cases, where we do need to set the color scheme on a portion of the hierarchy, we can override the colorScheme environment value manually, like we are doing in the example in this section.

# Setting toolbar color scheme

Starting from iOS 16 we can set a specific color scheme for a toolbar using toolbarColorScheme(_:in:) modifier.

It can be useful if we want to apply a custom toolbar background, that requires text and buttons in the toolbar to always be dark or light to stay visible.

Toolbar color scheme takes effect when toolbar background is visible. In the example below toolbar background will become indigo, when content is scrolled behind it. At that time, the text in the toolbar will become light even if device color scheme is light, since we set the dark color scheme for the toolbar.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(1...20, id: \.self) {
                Text("\($0)")
            }
            .listStyle(.plain)
            .navigationTitle("My List")
            
            .toolbarBackground(.indigo, in: .navigationBar)
            .toolbarColorScheme(.dark, in: .navigationBar)
        }
    }
}