4

I want to have a system/light/dark mode picker in a sheet, along with other settings.

Selecting a different scheme does correctly change the UI in the ContentView (behind the sheet), but the sheet itself retains the old scheme and must be dismissed and opened again.

What am I doing wrong?

Example:

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var viewManager: ViewManager
    @State var showSettingsSheet = false
    
    var body: some View {
        NavigationView {
            List {
                Text("Some content 1")
                Text("Some content 2")
            }
            .toolbar {
                ToolbarItem {
                    Button(action: {
                        showSettingsSheet.toggle()
                    }){
                        Image(systemName: "gearshape.fill")
                    }
                    .sheet(isPresented: $showSettingsSheet){SettingsSheet()}
                }
            }
        }
        .preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil)
    }
}
struct SettingsSheet: View {
    @Environment (\.presentationMode) var presentationMode
    @EnvironmentObject var viewManager: ViewManager

    var body: some View {
        NavigationView{
            GroupBox {
                Picker("Display Mode", selection: $viewManager.colorScheme) {
                    Text("System").tag("System")
                    Text("Light").tag("Light")
                    Text("Dark").tag("Dark")
                }
                .pickerStyle(.segmented)
            }
            .toolbar {
                ToolbarItem {
                    Button(action: {
                        presentationMode.wrappedValue.dismiss()
                    }){
                        Text("Done")
                    }
                }
            }
        }
    }
}
class ViewManager: ObservableObject {
    @AppStorage("colorScheme") var colorScheme: String = "System"
}
blu-Fox
  • 401
  • 6
  • 14

2 Answers2

3

You have to provide colorScheme to sheet manually as it is outside your view hierarchy, i.e.:

struct ContentView: View {
    @EnvironmentObject var viewManager: ViewManager
    @State var showSettingsSheet = false
    
    var body: some View {
        NavigationView {
            List {
                Text("Some content 1")
                Text("Some content 2")
            }
            .toolbar {
                ToolbarItem {
                    Button(action: {
                        showSettingsSheet.toggle()
                    }){
                        Image(systemName: "gearshape.fill")
                    }
                    .sheet(isPresented: $showSettingsSheet){
                      SettingsSheet()
                       .preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil)  // HERE
                    }
                }
            }
        }
        .preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil)
    }
}
cluelessCoder
  • 948
  • 6
  • 19
  • 1
    Almost there! This works correctly when switching to Light or Dark mode, but not when switching back to System. Any ideas? – blu-Fox Sep 26 '21 at 16:59
  • You're right. I've played with it and it just doesn't work on .sheet. Seems like one of many issues of iOS 15. I've even tried to manually set UIApplication.shared.windows.first?.overrideUserInterfaceStyle to ".unspecified" without any result. The issue only occurs until the sheet disappears. My recommendation would be to look into my answer in the following link (the – cluelessCoder Sep 26 '21 at 18:43
  • Have you found a workaround? – cyril Feb 06 '23 at 23:50
3

With a huge workaround, I did manage to make this work. It's probably worth reporting this to Apple though.

Something is wrong with getting the colorScheme update inside a sheet, but we can attach this colorScheme environment variable to a different published variable and then use that one in the sheet.

struct ContentView: View {
    
    @Environment(\.colorScheme) private var colorScheme
    @EnvironmentObject var settings: Settings

    var body: some View {
        ContentView()
        .preferredColorScheme(!settings.automaticAppearance ? (settings.lightAppearance ? .light : .dark) : nil)
        .environmentObject(settings)
        .onChange(of: colorScheme) { newColorScheme in
            // update color scheme to settings (from system environment variable)
            settings.systemColorScheme = newColorScheme
        }
    }
}
final class Settings: ObservableObject {
    
    @Environment(\.colorScheme) var colorScheme
    
    @Published var lightAppearance: Bool {
        didSet {
            UserDefaults.standard.set(lightAppearance, forKey: "light_appearance")
        }
    }
    
    @Published var automaticAppearance: Bool {
        didSet {
            UserDefaults.standard.set(automaticAppearance, forKey: "automatic_appearance")
        }
    }
    
    @Published var systemColorScheme: ColorScheme {
        didSet {
            UserDefaults.standard.set(systemColorScheme.hashValue, forKey: "system_color_scheme")
        }
    }
    
    init() {
        self.lightAppearance = UserDefaults.standard.object(forKey: "light_appearance") as? Bool ?? true
        self.automaticAppearance = UserDefaults.standard.object(forKey: "automatic_appearance") as? Bool ?? true
        self.systemColorScheme = UserDefaults.standard.object(forKey: "system_color_scheme") as? ColorScheme ?? .light
    }
    
}

I would like users to be able switch light mode and also select an 'automatic' option where the system color scheme is used. In my Settings sheet:

struct SettingsView: View {
    
    @Environment(\.colorScheme) var colorScheme
    @EnvironmentObject var settings: Settings
    
    var body: some View {
        
        List {
                
            Section(header: Text(NSLocalizedString("appearance", comment: "Appearance"))) {
                    
                Group {

                    // appearance
                    Toggle("Light mode", isOn: $settings.lightAppearance)
                        .onChange(of: settings.lightAppearance) { lightAppearance in
                            // if light/dark appearance was set, disable automatic appearance
                            settings.automaticAppearance = false
                        }
                        
                    Toggle(isOn: $settings.automaticAppearance) {
                        Text(NSLocalizedString("automatic", comment: "Automatic"))
                    }

                }

            }
      
        }
        .preferredColorScheme(settings.automaticAppearance ? settings.systemColorScheme : (settings.lightAppearance ? .light : .dark))
    }
}

So in short: use a different published variable to listen for in your sheet, because the environment's color scheme is bugged.