Skip to content

Instantly share code, notes, and snippets.

@chockenberry
Created September 11, 2024 18:36
Show Gist options
  • Save chockenberry/a2a23a12604e333b1c2a8b71e7a24155 to your computer and use it in GitHub Desktop.
Save chockenberry/a2a23a12604e333b1c2a8b71e7a24155 to your computer and use it in GitHub Desktop.
//
// ColorSchemeApp.swift
// ColorScheme
//
// Created by Craig Hockenberry on 9/11/24.
//
import SwiftUI
@main
struct ColorSchemeApp: App {
@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(preferredColorScheme)
}
.onChange(of: preferredColorScheme) { oldValue, newValue in
print("ColorSchemeApp: preferredColorScheme: \(oldValue) -> \(newValue)")
}
}
}
// NOTE: This allows @AppStorage to store an optional ColorScheme
extension ColorScheme: RawRepresentable {
public init?(rawValue: Int) {
guard let userInterfaceStyle = UIUserInterfaceStyle(rawValue: rawValue),
userInterfaceStyle != .unspecified,
let colorScheme = ColorScheme(userInterfaceStyle)
else {
//print("ColorScheme init: colorScheme = nil, rawValue = \(rawValue)")
return nil
}
//print("ColorScheme init: colorScheme = \(colorScheme), userInterfaceStyle = \(userInterfaceStyle)")
self = colorScheme
}
public var rawValue: Int {
let userInterfaceStyle = UIUserInterfaceStyle(self)
//print("ColorScheme rawValue: self = \(self), userInterfaceStyle = \(userInterfaceStyle)")
return userInterfaceStyle.rawValue
}
}
// NOTE: This provides a display name for an optional ColorScheme
extension ColorScheme? {
public var displayName: String {
if let self {
switch self {
case .dark: return "Dark"
case .light: return "Light"
@unknown default:
return "Unknown"
}
}
else {
return "System"
}
}
}
struct ContentView: View {
@State private var presentSettings = false
@State private var identityHack: Int = 0
@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?
var body: some View {
VStack(spacing: 40) {
Text(preferredColorScheme.displayName).font(.headline)
Button("Show Settings") {
presentSettings = true
}
}
.padding()
.sheet(isPresented: $presentSettings) {
SettingsView()
.id(identityHack) // an attempt to force an update by changing structural identity
.preferredColorScheme(preferredColorScheme)
.onChange(of: identityHack) { oldValue, newValue in
print("SettingView: identityHack: \(oldValue) -> \(newValue)")
}
}
.onChange(of: preferredColorScheme) { oldValue, newValue in
print("ContentView: preferredColorScheme: \(oldValue) -> \(newValue)")
identityHack += 1
}
}
}
struct SettingsView: View {
@Environment(\.dismiss) private var dismiss
@State private var path = NavigationPath()
@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?
var body: some View {
NavigationStack(path: $path) {
Form {
Section {
Picker("Theme", selection: $preferredColorScheme) {
Text("System").tag(nil as ColorScheme?)
Text("Dark").tag(ColorScheme.dark as ColorScheme?)
Text("Light").tag(ColorScheme.light as ColorScheme?)
}
}
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
}
}
}
#Preview {
ContentView()
}
@chockenberry
Copy link
Author

chockenberry commented Sep 11, 2024

The workaround is to change line 87 to the following:

.preferredColorScheme(preferredColorScheme ?? colorScheme)

And add the colorScheme environment to ContentView:

@Environment(\.colorScheme) private var colorScheme

The optional chaining on preferredColorScheme causes the current colorScheme to be set explicitly.

Thanks to Dimitri Bouniol for the inspriation.

@chockenberry
Copy link
Author

A full working solution is below:

//
//  ColorSchemeApp.swift
//  ColorScheme
//
//  Created by Craig Hockenberry on 9/11/24.
//

import SwiftUI

@main
struct ColorSchemeApp: App {
	@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?

    var body: some Scene {
        WindowGroup {
            ContentView()
				.preferredColorScheme(preferredColorScheme)
        }
    }
}

// NOTE: This allows @AppStorage to store an optional ColorScheme
extension ColorScheme: RawRepresentable {
	
	public init?(rawValue: Int) {
		guard let userInterfaceStyle = UIUserInterfaceStyle(rawValue: rawValue),
			  userInterfaceStyle != .unspecified,
			  let colorScheme = ColorScheme(userInterfaceStyle)
		else {
			return nil
		}

		self = colorScheme
	}
	
	public var rawValue: Int {
		let userInterfaceStyle = UIUserInterfaceStyle(self)
		return userInterfaceStyle.rawValue
	}

}

// NOTE: This provides a display name for an optional ColorScheme
extension ColorScheme? {

	public var displayName: String {
		if let self {
			switch self {
			case .dark: return "Dark"
			case .light: return "Light"
			@unknown default:
				return "Unknown"
			}
		}
		else {
			return "System"
		}
	}
	
}

struct ContentView: View {
	
	@State private var presentSettings = false
	
	@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?

	@Environment(\.colorScheme) private var colorScheme

	var body: some View {
		VStack(spacing: 40) {
			Text(preferredColorScheme.displayName).font(.headline)
						
			Button("Show Settings") {
				presentSettings = true
			}
		}
		.padding()
		.sheet(isPresented: $presentSettings) {
			// NOTE: Using a nil value for .preferredColorScheme() _will not_ update the view. The workaround
			// is to get the current colorScheme from the environment and use it instead (via optional chaining).
			SettingsView()
				.preferredColorScheme(preferredColorScheme ?? colorScheme)
		}
	}
}

struct SettingsView: View {
	
	@Environment(\.dismiss) private var dismiss
	
	@State private var path = NavigationPath()
		
	@AppStorage("preferredColorScheme") private var preferredColorScheme: ColorScheme?
	
	var body: some View {
		NavigationStack(path: $path) {
			Form {
				Section {
					Picker("Theme", selection: $preferredColorScheme) {
						Text("System").tag(nil as ColorScheme?)
						Text("Dark").tag(ColorScheme.dark as ColorScheme?)
						Text("Light").tag(ColorScheme.light as ColorScheme?)
					}
				}
			}
			.toolbar {
				ToolbarItem(placement: .confirmationAction) {
					Button("Done") {
						dismiss()
					}
				}
			}
		}
	}
}

#Preview {
	ContentView()
}

@kevinrpb
Copy link

For reference, this method is not fully working and I can't really tell why.

I recorded a short video to showcase the issue (iOS 18 simulator, but same behavior in my device). As you'll see, I can toggle the system color scheme in the beginning and the app changes. However, when I change the setting to 'Dark' and then back to 'System', the app stays dark and no longer updates when the system color scheme changes.

ColorSchemeNotWorking.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment