Dark mode toggle override - how to revert to system control?

Hi all,

I have a semi-working approach to manually toggle Dark Mode across an app in SwiftUI. The toggle overrides the System dark mode. However, does anyone know how I can add a button for giving an option to revert to System light/dark mode instead?

I am running Xcode beta in iOS14. See code below.
Many thanks!

Settings toggle
Code Block
@AppStorage("isDarkMode") var isDarkMode: Bool = true
var body: some View {
Form {
Toggle(isOn: $isDarkMode) {
                        Text("Darkmode")
                    }
}

Dark mode modifier definition
Code Block
public struct DarkModeViewModifier: ViewModifier {
@AppStorage("isDarkMode") var isDarkMode: Bool = true
public func body(content: Content) -> some View {
content
.environment(\.colorScheme, isDarkMode ? .dark : .light)
.preferredColorScheme(isDarkMode ? .dark : .light)
    }
}

This is the how I applied the dark mode modifier across the app
Code Block
@main
struct Lab_mate_2App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modifier(DarkModeViewModifier())
        }
    }
}


I ran into the same issue. This is more complex than it needs to be, but at present, the key is to (1) track the state of the device color scheme; (2) track the state of the end user's preferred color scheme choice (e.g. in a ObservableObject with a @Published variable; you need to track more than just isDarkMode like you did - auto/system, light, and dark); (3) independently track the end user's preferred color scheme choice in a @AppStorage variable, and (4) use colorScheme() vice preferredColorScheme().

As you can see below, I'm reacting to changes in the system color scheme using the new (in iOS 14) .onChange() functionality. If you use .preferredColorScheme() instead of .colorScheme(), the aforementioned .onChange() doesn't fire for some reason (possibly a bug). In my testing so far, when using .colorScheme(), the below .onChange() fires every time the end user changes the system level color scheme.

Here's a quick example:

Code Block
struct ContentView: View {
    #if os(iOS)
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
// This encapsulates the @AppStorage and @Publisher variables
// that I mentioned above
    @EnvironmentObject private var preferences:PreferencesStore
//
    @Environment(\.colorScheme) var deviceColorScheme: ColorScheme
    #endif
    @ViewBuilder var body: some View {
        Group {
            if preferences.isCompactInterface(idiom: horizontalSizeClass!) {
                TabBarContentView()]
// .colorScheme() instead of .preferredColorScheme()
                .colorScheme(preferences.colorScheme.systemColorScheme())
            } else {
                SideBarContentView()
                .colorScheme(preferences.colorScheme.systemColorScheme())
            }
            #else
// MacOS code here...
            #endif
        }.onChange(of: deviceColorScheme) { newValue in
            #if os(iOS)
            // Update the current device configuration
            PreferencesStore.deviceColorScheme = newValue
// If the end user is in 'auto' mode, meaning let the system dictate
// the color scheme...
            if preferences.appColorScheme == 0 {
                if PreferencesStore.deviceColorScheme == .light {
                    preferences.colorScheme = .light
                } else {
                    preferences.colorScheme = .dark
                }
            }
            #endif
        }.onAppear {
            #if os(iOS)
// Set the current device configuration
            PreferencesStore.deviceColorScheme = deviceColorScheme
            #endif
        }
    }
}


Code Block
public enum InternalColorScheme {
    case initial, auto, light, dark
    init(code:Int) {
        if code == 0 {
            self = .auto
        } else if code == 1 {
            self = .light
        } else {
            self = .dark
        }
    }
    public func systemColorScheme() -> ColorScheme {
        if self == .auto || self == .initial {
            return PreferencesStore.deviceColorScheme
        } else if self == .light {
            return .light
        } else {
            return .dark
        }
    }
}

Code Block
class PreferencesStore: ObservableObject {
    
    // ---------------------------------------
    // START: COLOR SCHEME
    // ---------------------------------------
    // 0: auto
    // 1: light
    // 2: dark
    //
    // See updateStoredColorScheme() below
    //
    @AppStorage(wrappedValue: 2, "colorScheme")
    public var appColorScheme:Int
    // Update this to change how the entire view hierarchy looks
    @Published public var colorScheme:InternalColorScheme = .initial
    // Updated in ContentView() via an onChange() listener
    public static var deviceColorScheme:ColorScheme = .light
    // ---------------------------------------
    // END: COLOR SCHEME
    // ---------------------------------------
    init() {
        // Default to what is stored in UserDefaults, which causes the entire
// view hierarchy the update via .colorScheme() because colorScheme
// is a @Published variable
        self.colorScheme = InternalColorScheme(code: appColorScheme)
    }
// call this when the end user wants to change the color scheme
    public func updateStoredColorScheme(colorScheme:InternalColorScheme) {
        if colorScheme == .auto {
// update UserDefaults
            appColorScheme = 0
// Change the appearance
            if PreferencesStore.deviceColorScheme == .light {
                self.colorScheme = .light
            } else {
                self.colorScheme = .dark
            }
        } else if colorScheme == .light {
            appColorScheme = 1
            self.colorScheme = .light
        } else {
            appColorScheme = 2
            self.colorScheme = .dark
        }
    }
}


You can easily update you modifier code to use colorScheme() and use the above code as a basis to implement the state management I talked about. Let me know if that doesn't make sense.

This implementation isn't perfect. I'm currently running into some edge cases when using .sheet().
Update: It’s better to use preferredColorScheme(), otherwise you need to style the status bar and some swiftui view’s don’t style properly with colorScheme().

So, my strategy is to cache the current device color scheme on load (by reading the .colorScheme environment variable) and basically revert the color scheme to that when the end user wants to go back to ‘auto’ mode. However, it seems that you ultimately need to pass nil to preferredColorScheme() to react to system color scheme changes (this isn’t documented), so you can kick off a background task to set this to nil after you revert to the cached original color scheme.

The only edge case where this doesn’t set the correct color scheme is if the system color scheme changes after your app launched.
Dark mode toggle override - how to revert to system control?
 
 
Q