Why preferredColorScheme has no effect on the view presented by the ColorPicker in SwiftUI?

58 views Asked by At

I have an app where I let the user choose the colorScheme, so that it can be left in adaptive mode or it can be forced to day or night mode, even if the system scheme of the device is different.

The issue is that the popover presented by the ColorPicker does not reflect the chosen ColorScheme, but only the system one (of the device).

The proposed code produces a list, which reflects automatically the preferredColorScheme, as an example. Then it produces a ColorPicker to set the background color and some tappable text to change the colorScheme to setColorScheme.

import SwiftUI

struct ColorPickerTest: View {
    
    @Environment(\.colorScheme) var colorScheme
    @State var setColorScheme: ColorScheme? = nil
    
    @State var selectedColor = Color.green
    
    var body: some View {
        ZStack {
            selectedColor.ignoresSafeArea()
            VStack {
                List {
                    ForEach(1..<6) { n in
                        Text("item \(n)")
                    }
                }
                .frame(height: 500)
                .cornerRadius(30)
                
                ColorPicker("Color", selection: $selectedColor)
                    .fixedSize().padding()
                    .background(colorScheme == .dark ? .blue : selectedColor)
                    .cornerRadius(30).padding()
                
                HStack {
                    Text("Theme:")
                    Text("Adaptive").onTapGesture { setColorScheme = nil }
                    Text("Night").onTapGesture { setColorScheme = .dark }
                    Text("Day").onTapGesture { setColorScheme = .light }
                }
            }
        }
        .preferredColorScheme(setColorScheme)
    }
}

#Preview {
    ColorPickerTest()
}

This works well, except for the popover presented by ColorPicker, in that case the setColorScheme has no effect and only reflects the system scheme, ignoring the imposed preferredColorScheme.

Does anyone have a solution or an explanation?

1

There are 1 answers

2
soundflix On

macOS solution:

I don't know how to have SwiftUI's ColorPicker automatically apply background color, but I can propose this little hack:

ColorPicker's window is a NSColorPanel that we can search in the app's window hierarchy. There we can set its properties.

struct ContentView: View {
    @State private var selectedColor: Color = .gray
    
    var body: some View {
        VStack {
            ColorPicker("Pick", selection: $selectedColor)
                .onAppear {
                    DispatchQueue.main.async {
                        guard let panel = colorPanel else { return }
                        /// Now we can access all NSWindow/NSPanel/NSColorPanel properties
                        print("\(panel.backgroundColor)") // "Optional(Catalog color: System windowBackgroundColor)"
                        panel.backgroundColor = NSColor(selectedColor)
                        // panel.titlebarAppearsTransparent = true
                        // panel.titleVisibility = NSWindow.TitleVisibility.hidden
                        // panel.title = "Random text"
                        print("\(panel.backgroundColor)")
                    }
                }
                .onChange(of: selectedColor) { newColor in
                    colorPanel?.backgroundColor = NSColor(selectedColor)
                }
        }
        .padding()
        .fixedSize()
        .background(selectedColor)
    }
}

Note that in onAppear we need to "wait" with DispatchQueue.main.async for the panel to exist in the app's windowsarray.

You will still have to see how to apply your adaptive color scheme, but that how to apply the color if needed.

You can see from the print output, that the panel's initial backgroundColor is "Catalog color: System windowBackgroundColor". So if you want to revert to have it repect the system's scheme, you'd have to set back to this value.