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() {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.forEach { $0.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:
- Intercepting SwiftUI Sheet Dismissal
- Debugging Swift Code: From print() to LLDB
- Stretchable Header in SwiftUI for Vertical and Horizontal ScrollView
- Organizing SwiftUI Views with TabContent and @TabContentBuilder
- Accessing UIWindow from SwiftUI
- Displaying a SwiftUI View Above a System Alert Using windowLevel
- Document Preview Options in SwiftUI
- Localizing Swift Packages in an iOS App
- Handling Non-Breaking Numbers in Dynamic Text
