If you have created a new SwiftUI project recently, you might have noticed that your AppDelegate & SceneDelegate files are gone. Instead it gets replaced by App struct which acts as the entry point for your app. In UIKit projects, to override the theme for whole app, we could access the overrideUserInterfaceStyle of window and set it to appropriate style. Let’s learn how to switch between themes in a SwiftUI way.

Setup

First we need to setup colors. Go to you project .xcassets file and create a color asset. You can define color for light & dark appearance in your color asset. For the sake of simplicity, this article will use two colors backgroundStyle1 and backgroundStyle2 setup as color assets. After setting up the colors, let’s create an enum to track our Theme. This enum also contains an extra property called colorScheme which we will use later.


enum Theme: Int {
    case light
    case dark
    
    var colorScheme: ColorScheme {
        switch self {
        case .light:
            return .light
        case .dark:
            return .dark
        }
    }
}

Source Of Truth

In SwiftUI world, we often hear about source of truth. When source of truth changes, our views should react to it and change its state. For our app, its app settings where we store user theme preference. When user selects particular theme, we update our settings and expect views to change their color scheme based on this value.

We also need to persist the user selected value. For that we use @AppStorage property wrapper which SwiftUI provides. There is a special reason to use this. From apple docs, @Appstorage is

A property wrapper type that reflects a value from UserDefaults and invalidates a view on a change in value in that user default.

What this simply means is, not only @AppStorage persists the value in UserDefaults but it also acts like a @State property i.e as soon as this property changes, the views can reflect this change. So lets create our source of truth:

class AppSettings: ObservableObject {

    static let shared = AppSettings() 
    
    @AppStorage("current_theme") var currentTheme: Theme = .light
}

When user selects light or dark theme, we will update this current theme value. As a result of this change, our view should change its color scheme. Let’s create a simple view to test this.


@main
struct MyApp: App {
    
    var body: some Scene {
        WindowGroup {
            FirstView()
        }
    }
}

...

struct FirstView: View {

    var body: some View {
        NavigationView {
            VStack {
                Rectangle()
                    .foregroundColor(Color("backgroundStyle2"))
                    .padding(24)
                
                Rectangle()
                    .foregroundColor(Color("backgroundStyle2"))
                    .padding(24)
                
                Rectangle()
                    .foregroundColor(Color("backgroundStyle2"))
                    .padding(24)
            }
            .background(Color("backgroundStyle1"))
            .navigationBarTitle("SwiftUI Themes", displayMode: .inline)
            .navigationBarItems(trailing: Button(action: {
                
                let currentTheme = AppSettings.shared.currentTheme
                AppSettings.shared.currentTheme = currentTheme == .light ? .dark : .light
                
            }, label: {
                Image(systemName: AppSettings.shared.currentTheme == .light ? "moon" : "moon.fill")
                    .resizable()
                    .scaledToFit()
            }))
        }
    }
}

In this view, when user taps on the navigation bar button, we toggle between light & dark theme. We update this value in AppSettings. When you press on that button now, nothing happens. This is because our view is unaware about the changes in our source of truth. We will fix this in next step.

Color Scheme

First lets talk about how we can change the color scheme in SwiftUI. It provides two modifiers for changing the color scheme i.e colorScheme & preferredColorScheme. If you look into their documentation, there is a small difference between these two modifiers.

colorScheme gets applied to view & its subviews. So if you apply colorScheme to VStack, everything inside VStack also gets same scheme.

preferredColorScheme is same as colorScheme but it also gets applied to nearest enclosing presentation. In simple word, its nearest parent. Since our FirstView is enclosed by WindowGroup and we want color scheme changes to be reflected in status bar, we are going to use this. Lets add the code to change the color scheme.


@main
struct MyApp: App {

    @ObservedObject var appSettings = AppSettings.shared
    
    var body: some Scene {
        WindowGroup {
            FirstView()
                .preferredColorScheme(appSettings.currentTheme.colorScheme)
        }
    }
}

Here, we create our AppSettings instance as an ObservedObject. This is so that our view can react to the changes published by AppSettings instance. Then we add preferredColorScheme modifier to our main view. The value for the ColorScheme parameter is obtained from the app settings.

That’s it. Now if you run the app, you can see that our view reacts to the theme changes.


Follow me on Twitter: @nrlnishan

Love this article? You can also buy me: A Coffee ☕