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 ☕