SwiftUI dark mode, light mode, and system theme


Greetings, traveler!

With SwiftUI, it’s super easy to change your app’s theme. I’m gonna show you how to change the color scheme and save the new value in UserDefaults using the @AppStorage property wrapper. By the way, in the last article, we talked about @Appstorage and how you can use it.

AppearanceKind

Let’s create an enumeration to contain options for the user interface styles. To select from the menu, we must mark our enumeration as CaseIterable and Identifiable and create some properties to store the values of the corresponding images, colors and names. We also need to specify that the rawValue of enumeration cases has an integer value so that we can save them in UserDefaults and use for setting desired app theme.

enum AppearanceKind: Int, CaseIterable {
    case system = 0
    case light = 1
    case dark = 2
}

extension AppearanceKind: Identifiable {
    var id: Self { self }
}

extension AppearanceKind {
    var title: String {
        switch self {
        case .system: "System"
        case .light: "Light Mode"
        case .dark: "Dark Mode"
        }
    }
    
    var imageName: String {
        switch self {
        case .system: "sun.haze.fill"
        case .light: "sun.max.fill"
        case .dark: "moon.stars.fill"
        }
    }
    
    var color: Color {
        switch self {
        case .system: .green
        case .light: .orange
        case .dark: .purple
        }
    }
}

ThemeManager

So, we gotta find the guy who’s gonna change the color scheme, right? That’s the ThemeManager. It should have a function that lets you change the userInterfaceStyle property value of the active window. We will use the enumeration we created earlier to modify the value of the property. The modified value will be stored in UserDefaults, so we will use the @AppStorage property wrapper to retrieve the value.

import SwiftUI

final class ThemeManager {

    @AppStorage("selectedAppearance") var selectedAppearance: AppearanceKind = .system

    func overrideDisplayMode() {
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        let window = windowScene?.windows.first
        
        window?.overrideUserInterfaceStyle = .init(rawValue: selectedAppearance.rawValue) ?? .unspecified
    }
    
}

View

Let’s create a View now and use the entities we created earlier. We can use the values inside the menu since our enumeration was marked as a CaseIterable. Each menu item will be a button that overrides the userInterfaceStyle value with the help of our ThemeManager. We’ll also define it when our View appears.

struct ContentView: View {
    
    @AppStorage("selectedAppearance") private var selectedAppearance: AppearanceKind = .system
    private var themeManager = ThemeManager()
    
    var body: some View {
        Menu {
            ForEach(AppearanceKind.allCases) { appearance in
                Button {
                    selectedAppearance = appearance
                } label: {
                    HStack {
                        Text(appearance.title)
                        
                        Image(systemName: appearance.imageName)
                            .renderingMode(.template)
                            .foregroundColor(appearance.color)
                    }
                }
            }
        } label: {
            Image(systemName: selectedAppearance.imageName)
                .renderingMode(.template)
                .font(.system(size: 25))
                .foregroundColor(selectedAppearance.color)
        }
        .onChange(of: selectedAppearance) { _, _ in
            themeManager.overrideDisplayMode()
        }
        .onAppear {
            themeManager.overrideDisplayMode()
        }
    }
    
}

Conclusion

So that’s it! We can now easily manage our app’s theme. Hope you found this tutorial useful. 

See you soon!