An under-utilized feature I always appreciate when I see it is the ability to select an appearance for an app independent of the system appearance.
If you search how to set the color scheme of a view in SwiftUI, you’ll probably come across preferredColorScheme
which looks great but doesn’t work how we’d like for this feature. Instead, we are going to make use of overrideUserInterfaceStyle
We’ll start off by creating our AppearanceOptions
enum and make it conform to String
and CaseIterable
for use in our form later on.
enum AppearanceOptions: String, CaseIterable {
case system, light, dark
}
And then we’ll create the most important part, our AppearanceController
and set the default appearance to .System
in UserDefaults
using the @AppStorage
property wrapper
class AppearanceController {
static let shared = AppearanceController()
@AppStorage("appAppearance") var appAppearance: AppearanceOptions = .system
}
Next we’ll need to get the correct UIUserInterfaceStyle
to use with overrideUserInterfaceStyle
so we’ll create a computed property in our AppearanceController
that checks our appearance saved in UserDefaults
.
var appearance: UIUserInterfaceStyle {
switch appAppearance {
case .system:
return .unspecified // Uses appearance set by user in Settings
case .light:
return .light
case .dark:
return .dark
}
}
And finally we’ll create our setAppearance
function.
func setAppearance() {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
guard let window = windowScene?.windows.first else { return }
window.overrideUserInterfaceStyle = appearance
}
Now in ContentView
we’ll create a Picker for our appearance options.
struct ContentView: View {
@AppStorage("appAppearance") var appAppearance: AppearanceOptions = .system
var body: some View {
Form {
Picker("Appearance", selection: $appAppearance) {
// This is where CaseIterable comes into play, allowing us to loop over our AppearanceOptions enum
ForEach(AppearanceOptions.allCases, id: \.self) { option in
Text(option.rawValue.capitalized)
}
}
.pickerStyle(.inline)
}
}
}
On that Picker we’ll add an onChange
modifier to call setAppearance
when the user changes the selected appearance.
.onChange(of: appAppearance) { _ in
AppearanceController.shared.setAppearance()
}
Now if you test your app, you should be able to set any color scheme, regardless of your system settings. But you might notice a problem if you close the app and reopen it.
If you selected Dark
and your system appearance is set to Light
(or vice-versa) you’ll see that the Picker reflects your selection but the appearance hasn’t changed. That’s because we forgot an important step: you need to call setAppearance
when the app loads.
@main
struct AppearanceDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
AppearanceController.shared.setAppearance()
}
}
}
}