Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I compare 2 UIColor objects which are kCGColorSpaceModelRGB and UIExtendedSRGBColorSpace instances in logs?

Now I'm really confused. Heres how the variable gets instantiated :

Utils.redColor = UIColor(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue)/255.0, alpha: alpha) 

And here I enumerate a Attribute text's attributes to skip the color if it's equals to Utils.redColor :

    text.enumerateAttributes(in: NSRange(0..<text.length), options: []) { (attributes, range, _) -> Void in
                            for (attribute, object) in attributes {
                                if let textColor = object as? UIColor{
                                     NSLog("textColor = \(textColor) red = \(Utils.redColor!)") 
             if (!textColor.isEqual(Utils.redColor!)){
//I need to repaint any textColor other than red
             text.setAttributes(textAttributes , range: range)
            }
          }

So, as you see in this code textColor is a UIColor object as well, but the log says:

textColor = kCGColorSpaceModelRGB 0.666667 0.172549 0.172549 1 red = UIExtendedSRGBColorSpace 0.666667 0.172549 0.172549 1

Which are two exact colors , but being instances of two different classes. This is totally confusing for both of them are UIColor class's objects!

This comparison never triggers although it worked well in Swift2

How do I fix it and why this problem ever occurs??

like image 371
Vlad Alexeev Avatar asked Dec 19 '16 15:12

Vlad Alexeev


1 Answers

Welcome to the wild and wooly world of wide color and color management.

Your two colors aren't equal, per isEqual (or Swift ==, which runs through isEqual for ObjC classes that have it), because they have different color spaces. (They aren't different classes; the first item in UIColor.description is an identifier for the color space, or where the color space doesn't have a name, the model for the color space — that is, whether it's RGB-based, CMYK-based, grayscale, etc.)

Without a color space to define them as a color, the four component values of a color have no reliable meaning, so isEqual uses both the component values and the color space to test for equality.

Aside on color spaces (skip down for solutions)

Your color created with UIColor init(red:green:blue:alpha:) uses the "Extended sRGB" color space. This color space is designed to support wide color displays (like the P3 color display in iPhone 7, iPad Pro 9.7", iMac late-2015, MacBook Pro late-2016, and probably whatever else comes next), but be component-value compatible with the sRGB color space used on other devices.

For example, sRGB 1.0, 0.0, 0.0 is the "red" you're probably most used to... but if you create a color in the P3 color space with RGB values 1.0, 0.0, 0.0 you get much much redder. If you have an app where you need to support both sRGB and P3 displays and work directly with color components, this can get confusing. So the Extended sRGB space lets the same component values mean the same thing, but also allows colors outside the sRGB gamut to be specified using values outside the 0.0-1.0 range. For example, the reddest that Display P3 can get is expressed in Extended sRGB as (roughly) 1.093, -0.227, -0.15.

As [the docs for that initializer note, for apps linked against the iOS 10 or later SDK, init(red:green:blue:alpha:) creates a color in the Extended sRGB color space, but for older apps (even if they're running on iOS 10) it creates a color in a device-specific RGB space (which you can generally treat as equivalent to sRGB).

Dealing with different color spaces

So, either your color-replacing code or whatever code is creating the colors in your attributed string need to be aware of color spaces. There are a few possible ways to deal with this; pick the one that works best for you:

  1. Make sure both your string-creation code and your color-replacement code are using the same device-independent color space. UIColor doesn't provide a lot of utilities for working with color spaces, so you can either use Display P3 (on iOS 10 and up), or drop down to CGColor:

    let sRGB = CGColorSpace(name: CGColorSpace.sRGB)!
    let cgDarkRed = CGColor(colorSpace: sRGB, components: [0.666667, 0.172549, 0.172549, 1])!
    let darkRed = UIColor(cgColor: cgDarkRed)
    
    // example creating attributed string...
    let attrString = NSAttributedString(string: "red", attributes: [NSForegroundColorAttributeName : darkRed])
    
    // example processing text...
    let redAttributes = [NSForegroundColorAttributeName: darkRed]
    text.enumerateAttributes(in: NSRange(0..<attrString.length)) { (attributes, range, stop) in
        for (_, textColor) in attributes where (textColor as? UIColor) != darkRed {
            text.setAttributes(redAttributes , range: range)
        }
    }
    
  2. If you can't control the input colors, convert them to the same color space before comparing. Here's a UIColor extension to do that:

    extension UIColor {
        func isEqualWithConversion(_ color: UIColor) -> Bool {
            guard let space = self.cgColor.colorSpace
                else { return false }
            guard let converted = color.cgColor.converted(to: space, intent: .absoluteColorimetric, options: nil)
                else { return false }
            return self.cgColor == converted
        }
    }
    

    (Then you can just use this function in place of == or isEqual in your text processing.)

  3. Just get at the raw component values of the colors and compare them directly, based on the assumption that you know the color spaces for both are compatible. Sort of fragile, so I recommend against this option.

like image 159
rickster Avatar answered Nov 16 '22 04:11

rickster