How to provide a ‘System’ appearance choice in SwiftUI?

Viewed 294

Main Question

Sorry for the long title. I’m trying to figure out how to provide a choice to user’s for setting the ColorScheme of the app to ‘System’.

As in, have the app default to the system’s dark mode / light mode. I know UIKit has a way to set the style to .unspecified but I’m not sure how to access that in SwiftUI.

I already tried setting .preferredColor() to nil and it kind of sort of works but sometimes it doesn’t. I’m new to managing state in Swift so I’m definitely doing something wrong.

Side Question:
When I set .preferredColor() to say .dark - the modal settings sheet I have doesn’t update? It always stays the system colour. I have .preferredColor() triggering on the ContentView() in the ...App.swift file.

Update

I solved my Side Question by just adding .preferredColor() to the modal sheet as well. It's not ideal but it does work. However I'm still unable to figure out how to set a .system preference in SwiftUI

1 Answers

I ended up finding two solutions to this problem. The first was found by jinjie and the other by me. Both solutions can be viewed here - repo. created by rizwankce.

Solution

What I ended up doing was using the package Introspect to grab the UIViewController off the main view I have using a ViewModifier.

Within the ViewModifier I just used .introspectViewController and that gave me the UIViewController so that I could then set UIViewController.overrideUserInterfaceStyle = .unspecified like this. Then I just did this conditionally based on a Picker not seen below. ⤵


/// All themes
enum Themes: String {
    case Dark
    case Light
    case System
}

// MARK: - Theme Switch

struct ThemeSwitch: ViewModifier {
    let appStorage: String

    func body(content: Content) -> some View {
        content
            .introspectViewController { UIViewController in
                switch appStorage {
                case Themes.System.rawValue: UIViewController.overrideUserInterfaceStyle = .unspecified
                case Themes.Dark.rawValue: UIViewController.overrideUserInterfaceStyle = .dark
                case Themes.Light.rawValue: UIViewController.overrideUserInterfaceStyle = .light
                default: UIViewController.overrideUserInterfaceStyle = .unspecified
                }
            }
    }
}

// MARK: - Extensions

extension View {
    func themeSwitch(appStorage: String) -> some View {
        modifier(ThemeSwitch(appStorage: appStorage))
    }
}

I just stored the desired theme as a String into a @State object which also updated an @AppStorage object so the app always had the selection no matter what.

You don't have to use Introspect and could instead create your own wrapper but I already had this package installed so I figured why not.

Alternative Solutions

The other solution is to make a view modifier and display the modified view conditionally. The condition is based on an optional array, dictionary, etc. of type ColorScheme. It either unwraps the optional if it's not nil or it just shows the unmodified view. You can find it at the repo. mentioned above under closed issues or pull requests.

In the future hopefully Apple will provide a way to do this natively but for now this will do! I'll try to update this answer in the future if something better comes along.

Related