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.
Post
Replies
Boosts
Views
Activity
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:
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
}
}
}
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
}
}
}
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().
Thanks for submitting the bug report. I'm getting the same behavior on beta 5.
I’ll second that request. I would definitely be interested in access to the underlying on-device api that the new iOS 14 translate app uses. I’m currently using google translate via firebase, but would prefer to get rid of this dependency.
I'm also trying to figure out how to do this. No luck as of yet.