0

I have a ColorPicker in my SwiftUI app that allows me to select the color of some shapes in a Canvas view. When black is selected in light mode or white is selected in dark mode I want to replace the selected color with SwiftUI's dynamic Color.primary value so that when I change the color theme the foreground color is automatically inverted. Otherwise the shapes would turn invisible in front of the same colored background.

The color selected with the ColorPicker view is saved as a standard SwiftUI Color. Nonetheless I could not figure out a way to check if black or white is selected. It seems like the ColorPicker creates the SwiftUI colors through UIColor and CGColor somehow. It could also have something to do with mismatching color spaces I guess.

I tried it in several different ways but couldn't get it to work. Maybe I'm stupid and there's an obvious solution but I just couldn't find it. Thanks in advance for any help!

Here is what I tried (the ColorPicker has a binding to the baseColor property):

@Published var baseColor: Color {
    didSet {
        print("Hello world!") // prints as expected when setting the color

        if baseColor == Color.black {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        if baseColor == Color(red: 0, green: 0, blue: 0) {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        if baseColor == Color(red: 0, green: 0, blue: 0, opacity: 1) {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        if baseColor == Color(white: 0) {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        if baseColor == Color(.displayP3, red: 0, green: 0, blue: 0, opacity: 1) {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        if baseColor == Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 1) {
            baseColor = .primary
            print("Base color changed!") // not called when color is set to black
        }

        ...
    }
}

When I print baseColor to the console I get "kCGColorSpaceModelRGB 0 0 0 1".

loki259
  • 41
  • 7
  • As you alluded to, the main issue is comparing two `Color` in a common color space or equivalent scheme. See for example: https://stackoverflow.com/questions/48081748/how-to-compare-two-cgcolor-in-swift-4 and https://stackoverflow.com/questions/56994464/how-to-convert-uicolor-to-swiftui-s-color There are other posts and info on how to compare `Color` and/or convert them to a comparable system. – workingdog support Ukraine Jun 21 '22 at 02:21
  • deleted my answer of compare the Colors, as it is not working and may not be relevant. – workingdog support Ukraine Jun 22 '22 at 20:30
  • You nonetheless pointed me in the right direction with your now deleted answer. The approach of comparing colors through their components while also allowing a small degree of deviation to account for the rounded float values produced by the color picker really helped me a lot in finding a solution for my problem. Thank you very much for all your support! – loki259 Jun 23 '22 at 10:17

1 Answers1

0

As @workingdog pointed out to me, the best approach to comparing colors here is converting them to a comparable system by taking their color components.

SwiftUI‘s Color type doesn‘t grant access to its components out of the box but by converting to UIColor we get methods for retrieving the color’s rgb, hsb and grayscale components.

By using this trick we can extend Color and add properties for conveniently getting the components we need in our comparisons:

extension Color {
    var rgbComponents: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) {
        var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
        UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a)
        return (r, g, b, a)
    }
    
    var hsbComponents: (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, opacity: CGFloat) {
        var (h, s, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
        UIColor(self).getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        return (h, s, b, a)
    }
    
    var grayscaleComponents: (white: CGFloat, opacity: CGFloat) {
        var (w, o): (CGFloat, CGFloat) = (0, 0)
        UIColor(self).getWhite(&w, alpha: &o)
        return (w, o)
    }
}

As I just want to check if black or white is selected for knowing when to adjust the color on theme change, getting the grayscale components is sufficient in my use case.

We can then use these components to compare the colors selected in the color picker. I found that the values generated by moving the sliders in the picker are sometimes a bit inaccurate however. For instance, selecting pure black by setting all rgb values to 0 can result in rgb components that are actually slightly above zero, possibly due to inaccurate rounding of float values. I accounted for this by comparing the component values to a small range of values respectively:

func updateColorScheme() {
    // set the theme
    let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
    scene?.keyWindow?.overrideUserInterfaceStyle = theme
    
    // update the selected color
    let isDarkMode = UITraitCollection.current.userInterfaceStyle == .dark
    if isDarkMode && baseColor.grayscaleComponents.white < 0.0001 {
        baseColor = .white
    }
    if !isDarkMode && baseColor.grayscaleComponents.white > 0.9999 {
        baseColor = .black
    }
}
loki259
  • 41
  • 7