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 let 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!
Check out other posts:
- Modular Form Validation in SwiftUI
- Simplifying Focus Management in SwiftUI with a Custom ViewModifier
- InlineArray – A Fixed-Size Arrays in Swift
- Weak let for Safer Immutable References
- Observing Value Changes with Observations in Swift 6.2
- Making String Interpolation Smarter in Swift 6.2: Default Values for Optionals
- Raw Identifiers in Swift 6.2
- How to use Debounce in SwiftUI or in Observable classes
- A Pull-Cord Theme Switcher in SwiftUI