Issue comparing UIColors in Swift
You can use "==" or isEqual. I have just tested both of them and they work fine:
let redColor = UIColor.redColor()
let greenColor = UIColor.greenColor()
let blueColor = UIColor.blueColor()
let testColor = UIColor.greenColor()
println( testColor == redColor ) // false
println( testColor == greenColor ) // true
println( testColor == blueColor ) // false
println( testColor.isEqual(redColor) ) // false
println( testColor.isEqual(greenColor) ) // true
println( testColor.isEqual(blueColor) ) // false
I have just reproduced the issue only happens after extracting the SKSpriteNode color and as you said only with fraction colors. You can work your way around this issue comparing the color description as follow:
let blue_color = UIColor(red: 122/255, green: 180/255, blue: 190/255, alpha: 1)
let yellow_color = UIColor(red: 253/255, green: 213/255, blue: 123/255, alpha: 1)
let red_color = UIColor(red: 238/255, green: 116/255, blue: 71/255, alpha: 1)
func randomColorController() -> UIColor {
let random = arc4random_uniform(3) + 1
switch random {
case 1:
return blue_color
case 2:
return red_color
case 3:
return yellow_color
default:
return UIColor.clearColor()
}
}
let square = SKSpriteNode(color: randomColorController(), size: CGSize(width: 30, height: 30))
if square.color.description == blue_color.description {
println(true)
}
if square.color.description == red_color.description {
println(true)
}
if square.color.description == yellow_color.description {
println(true)
}
How to compare colors in swift
You don't compare colors using the ==
operator. You do it like this and you need the !
to unwrap the optional color:
if sender.backgroundColor!.isEqual(UIColor.redColor()) {
}
Also, remove the extraneous =
in your assignment statement. It should be:
sender.backgroundColor = UIColor.whiteColor()
Comparing UIColor incorrect only for black and gray
If you try to log you checkColor you'll see that it's of type UIExtendedGrayColorSpace, and your fontColor is of type UIExtendedSRGBColorSpace.
The solution is to init your checkColor with and RGB color (I tried to cast your checkColor as rgb but didn't find a solution). So here is what I got :
let checkColor = UIColor.black
let checkColorRGB = UIColor.init(red: 0, green: 0, blue: 0, alpha: 1)
let label = SKLabelNode(text: "foo")
label.fontColor = UIColor.black
print(label.fontColor == checkColor)
print(label.fontColor == checkColorRGB)
print(label.fontColor!)
print(checkColor)
print(checkColorRGB)
Tested on playground, you'll see that the second print log true
How to compare UIColors?
Have you tried [myColor isEqual:someOtherColor]
?
Compare two UIColors (tap location in UIImageView vs assets catalog color)
Don't do this if you're not familiar with the following topics. I'm going to show you one way, a very simplified one, but there will be gotchas.
Resources
Something to read upfront or at least be aware/familiar with it.
- Color related
- Color space
- Color difference
- Color management (especially Color transformation)
- WWDC 2017 - 821 - Get Started with Display P3
- Float related
- What every computer scientist should know about floating-point arithmetic
Problem #1 - Which color do you want?
Your UIImageView
can be fully opaque, transparent, partially opaque, transparent, ... Let's say that there's a view with yellow color below the UIImageView
and the UIImageView
isn't opaque and alpha is set to 50%. What color do you want? Original image color? Rendered color (mixed with the yellow one)?
I assume the one mixed with the yellow. Here's the code to get the right color.
Objective-C:
@interface UIView(ColorAtPoint)
- (UIColor *)colorAt:(CGPoint)point;
@end
@implementation UIView(ColorAtPoint)
- (UIColor *)colorAt:(CGPoint)point {
UIView *targetOpaqueView = self;
while (!targetOpaqueView.isOpaque && targetOpaqueView.superview != nil) {
targetOpaqueView = targetOpaqueView.superview;
}
CGPoint targetPoint = [self convertPoint:point toView:targetOpaqueView];
unsigned char pixel[4] = {0};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
return nil;
}
CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
if (context == NULL) {
CGColorSpaceRelease(colorSpace);
return nil;
}
CGContextTranslateCTM(context, -targetPoint.x, -targetPoint.y);
[targetOpaqueView.layer renderInContext:context];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
UIColor *color = [UIColor colorWithRed:pixel[0]/255.0
green:pixel[1]/255.0
blue:pixel[2]/255.0
alpha:pixel[3]/255.0];
return color;
}
@end
Swift:
extension UIView {
func color(at point: CGPoint) -> UIColor? {
var targetOpaqueView: UIView = self
// Traverse the view hierarchy to find a parent which is opaque
while !targetOpaqueView.isOpaque && targetOpaqueView.superview != nil {
targetOpaqueView = targetOpaqueView.superview!
}
// Convert the point from our view to the target one
let targetPoint: CGPoint = convert(point, to: targetOpaqueView)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
var pixel: [UInt8] = [0, 0, 0, 0]
guard let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
context.translateBy(x: -targetPoint.x, y: -targetPoint.y)
// Render the target opaque view to get all the possible transparency right
targetOpaqueView.layer.render(in: context)
return UIColor(red: CGFloat(pixel[0])/255.0, green: CGFloat(pixel[1])/255.0, blue: CGFloat(pixel[2])/255.0, alpha: CGFloat(pixel[3])/255.0)
}
}
This function should return a color with alpha component set to 1.0
. It has to be 1.0
otherwise we can't continue. Why? Imagine that the UIImageView
alpha is set to 50% -> we wont traverse the view hierarchy (this targetOpaqueView
dance) -> we will get a color with alpha component close to 0.5
-> not of any use, what's below? White? Black? Orange?
Problem #2 - Device
Every device is different and can display different range of colors - iPads, iPhones, ... This also includes other device types like computer displays, printers, ... Because I don't know what are you trying to achieve in your application, take it as a reminder - same color, but different look on every device.
Problem #3 - Color profile
Here's the comparison of the Display P3 profile and Generic sRGB. You can compare more profiles just by launching ColorSync Utility on your macOS. It demonstrates that the color will differ when you convert a color from one space to another one.
Problem #4 - Color conversion
Quotes from the Color transformation chapter:
Color transformation, or color space conversion, is the transformation of the representation of a color from one color space to another.
&
In nearly every translation process, we have to deal with the fact that the color gamut of different devices vary in range which makes an accurate reproduction impossible.
This is sort of complicated process, but it depends on many things (color spaces, ...). It can involve posterization (16-bit -> 8-bit for example), it depends on your rendering intent (relative colorimetric, perceptual, ...), etc.
Very simplified example - you have an image with red color (#FF0000
) and there's Generic sRGB profile assigned to it. iPhone is going to display it (P3) and you'll see different color. More precisely, if you get RGB component values, they'll differ.
CoreGraphics.framework provides color conversion function - converted(to:intent:options:)
. You have to pass:
- the destination color space,
- intent = the mechanism to use to match the color when the color is outside the gamut of the new color space,
- options (just pass
nil
).
Objective-C:
@interface UIColor(ColorSpaceConversion)
- (UIColor *)convertedToColorSpace:(CGColorSpaceRef)colorSpace;
- (UIColor *)convertedToColorSpace:(CGColorSpaceRef)colorSpace inten:(CGColorRenderingIntent)intent;
@end
@implementation UIColor(ColorSpaceConversion)
- (UIColor *)convertedToColorSpace:(CGColorSpaceRef)colorSpace {
return [self convertedToColorSpace:colorSpace inten:kCGRenderingIntentDefault];
}
- (UIColor *)convertedToColorSpace:(CGColorSpaceRef)colorSpace inten:(CGColorRenderingIntent)intent {
CGColorRef converted = CGColorCreateCopyByMatchingToColorSpace(colorSpace, intent, self.CGColor, NULL);
if (converted == NULL) {
return nil;
}
UIColor *color = [[UIColor alloc] initWithCGColor:converted];
CGColorRelease(converted);
return color;
}
@end
Swift:
extension UIColor {
func converted(toColorSpace colorSpace: CGColorSpace, intent: CGColorRenderingIntent = .defaultIntent) -> UIColor? {
guard let converted = cgColor.converted(to: colorSpace, intent: intent, options: nil) else {
return nil
}
return UIColor(cgColor: converted)
}
}
An example:
- Component values (RGBA) of the Extended sRGB color (
#CC3333
): 0.8 0.2 0.2 1 - Same color converted to Extended Linear sRGB: 0.603827 0.0331048 0.0331048 1
- Same color converted to Display P3: 0.737027 0.252869 0.228974 1
Problem #5 - Custom color
You can create colors to compare against in two ways:
- Xcode assets catalog
UIColor
initializer
Xcode assets catalog allows you to specify color for different devices, gamuts or you can select custom content (Display P3, sRGB, Extended Range sRGB, ...).
UIColor
initializer allows you to specify color in (not just):
- Display P3
- Extended Range sRGB (iOS >= 10)
Be aware how do you create a color to compare against. RGB component values differ in various color spaces.
Problem #6 - Color comparison
As you are aware of how it works now, you can see that there's some math involved in a way that the converted color won't precisely match. I mean - two colors, converted to the same color space - you can't still compare components (RGB) with simple equality operator. Precision is lost due to conversion and even if not - remember What every computer scientist should know about floating-point arithmetic?
There's a way how to achieve what you want and it's called Color difference. In other words - you'd like to calculate two colors distance.
Objective-C:
@interface UIColor(EuclideanDistance)
- (CGFloat)euclideanDistanceTo:(UIColor *)other;
@end
@implementation UIColor(EuclideanDistance)
- (CGFloat)euclideanDistanceTo:(UIColor *)other {
CIColor *ciColor = [[CIColor alloc] initWithColor:self];
CIColor *ciColorOther = [[CIColor alloc] initWithColor:other];
if (ciColor.numberOfComponents != ciColor.numberOfComponents) {
NSException *exception = [NSException exceptionWithName:NSInvalidArgumentException
reason:@"Colors differ in numberOfComponents"
userInfo:nil];
@throw exception;
}
if (ciColor.alpha != 1.0 || ciColorOther.alpha != 1.0) {
NSException *exception = [NSException exceptionWithName:NSInvalidArgumentException
reason:@"Transparent colors are not supported"
userInfo:nil];
@throw exception;
}
CGFloat dr = ciColorOther.red - ciColor.red;
CGFloat dg = ciColorOther.green - ciColor.green;
CGFloat db = ciColorOther.blue - ciColor.blue;
return sqrt(dr * dr + dg * dg + db * db);
}
@end
Swift:
extension UIColor {
func euclideanDistance(to other: UIColor) -> CGFloat? {
let ciColor = CIColor(color: self)
let ciColorOther = CIColor(color: other)
guard ciColor.numberOfComponents == ciColorOther.numberOfComponents,
ciColor.alpha == 1.0, ciColorOther.alpha == 1.0 else {
return nil;
}
let dr = ciColorOther.red - ciColor.red
let dg = ciColorOther.green - ciColor.green
let db = ciColorOther.blue - ciColor.blue
return sqrt(dr * dr + dg * dg + db * db)
}
}
- Display P3: (0.692708 0.220536 0.201643 1)
- Display P3: (0.692708 0.220536 0.198637 1)
- Euclidean distance: 0.0030061900627510133
We're able to calculate Euclidean distance of two colors, let's write a simple function to check if two colors matches with some tolerance:
Objective-C:
@interface UIColor(Matching)
- (BOOL)matchesTo:(UIColor *)other;
- (BOOL)matchesTo:(UIColor *)other euclideanDistanceTolerance:(CGFloat)tolerance;
@end
@implementation UIColor(Matching)
- (BOOL)matchesTo:(UIColor *)other {
return [self matchesTo:other euclideanDistanceTolerance:0.01];
}
- (BOOL)matchesTo:(UIColor *)other euclideanDistanceTolerance:(CGFloat)tolerance {
CGFloat distance = [self euclideanDistanceTo:other];
return distance <= tolerance;
}
@end
Swift:
extension UIColor {
func matches(to other: UIColor, euclideanDistanceTolerance tolerance: CGFloat = 0.01) -> Bool? {
guard let distance = euclideanDistance(to: other) else {
return nil
}
return distance <= tolerance
}
}
All the pieces together
- I took a screenshot of your image
- Did open it in the Pixelmator and exported for web (sRGB, no profile associated)
- Dragged & dropped into the assets catalog
- Picked the red color (
#C02A2B
) from the image - Did add
CustomRedColor
into the assets catalog- Gamut Any, Content sRGB, 8-bit Hexadecimal
#C02A2B
- Gamut Any, Content sRGB, 8-bit Hexadecimal
- Made a simple view controller with tap gesture handler utilizing all the functions we have discussed
Objective-C:
@interface ViewController ()
@property (nonatomic, strong) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) IBOutlet UIView *leftView;
@property (nonatomic, strong) IBOutlet UIView *rightView;
@end
@implementation ViewController
- (IBAction)handleTapGesture:(UITapGestureRecognizer *)sender {
CGPoint location = [sender locationInView:self.imageView];
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3);
if (colorSpace == NULL) {
return;
}
UIColor *assetsCatalogRedColor = [UIColor colorNamed:@"CustomRedColor"];
UIColor *redColor = [assetsCatalogRedColor convertedToColorSpace:colorSpace];
UIColor *tappedColor = [[self.imageView colorAt:location] convertedToColorSpace:colorSpace];
CGColorSpaceRelease(colorSpace);
self.leftView.backgroundColor = tappedColor;
self.rightView.backgroundColor = redColor;
if (redColor == nil || tappedColor == nil) {
return;
}
@try {
BOOL matches = [tappedColor matchesTo:redColor];
NSLog(@"Tapped color matches CustomRedColor: %@", matches ? @"true" : @"false");
}
@catch (NSException *exception) {
NSLog(@"Something went wrong: %@", exception);
}
}
@end
Swift:
class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView!
@IBOutlet var leftView: UIView!
@IBOutlet var rightView: UIView!
private let assetsCatalogRedColor: UIColor = UIColor(named: "CustomRedColor")!
@IBAction
func handleTap(sender: UITapGestureRecognizer) {
let location = sender.location(in: imageView)
guard let colorSpace = CGColorSpace(name: CGColorSpace.displayP3),
let redColor = assetsCatalogRedColor.converted(toColorSpace: colorSpace, intent: .defaultIntent),
let tappedColor = imageView.color(at: location)?.converted(toColorSpace: colorSpace, intent: .defaultIntent) else {
return
}
let matches = tappedColor.matches(to: redColor) ?? false
print("Tapped color matches CustomRedColor: \(matches)")
leftView.backgroundColor = tappedColor
rightView.backgroundColor = redColor
}
}
- Tapped the bottom red circle
- And I've got
Tapped color matches CustomRedColor: true
Conclusion
It's not that easy to get colors right (define, get, compare, convert, ...). Tried to point out important things while keeping it simple, but you should be able to do it now. Don't forget to correctly create a color, convert to a proper color space, pick a tolerance which suits your application needs, etc.
Here's the public GitHub Gist which contains both Objective-C & Swift view controller implementations.
Addendum
The color space conversion is not necessary, because the [UIColor getRed:green:blue:alpha:]
documentation says:
If the color is in a compatible color space, the color is converted into RGB format and its components are returned to your application. If the color is not in a compatible color space, the parameters are unchanged.
&
red (green, blue) - On return, the red component of the color object. On applications linked for iOS 10 or later, the red component is specified in an extended range sRGB color space and can have any value. Values between 0.0 and 1.0 are inside the sRGB color gamut. On earlier versions of iOS, the specified value is always between 0.0 and 1.0.
Color component values should be converted for you if you use this function. But I kept the conversion code in this answer to demonstrate what's going on under the hood.
Comparing equality of [UIColor colorWithPatternImage:]
Using Private APIs
This took some reverse engineering of CoreGraphics
but I was able to find one private method _CGPatternGetImage
which appears to return the image.
You'll need to include the following headers:
#include <dlfcn.h>
@import CoreGraphics;
Create a function pointer:
typedef CGImageRef (*CGPatternGetImage)(CGPatternRef pattern);
And then access the function:
-(void)comparePatterns
{
void *handle = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", RTLD_NOW);
CGPatternGetImage getImage = (CGPatternGetImage) dlsym(handle, "CGPatternGetImage");
UIColor *aColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"pattern1"]];
UIColor *bColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"pattern1"]];
UIColor *cColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"pattern2"]];
NSData *aImageData = UIImagePNGRepresentation([UIImage imageWithCGImage:getImage(CGColorGetPattern(aColor.CGColor))]);
NSData *bImageData = UIImagePNGRepresentation([UIImage imageWithCGImage:getImage(CGColorGetPattern(bColor.CGColor))]);
NSData *cImageData = UIImagePNGRepresentation([UIImage imageWithCGImage:getImage(CGColorGetPattern(cColor.CGColor))]);
NSLog(@"Should be true: %d",[aImageData isEqual:bImageData]);
NSLog(@"Should be false: %d",[aImageData isEqual:cImageData]);
}
You probably don't want to access any private APIs in a production app but this might be useful for testing.
Using Associative References
If this is going on the App Store then a better solution could be creating a category for UIColor
and give it an associative reference to store the pattern name or whatever is easiest for you to compare. This won't compare the actual images at all so it's possible that if you don't set the correct data to identify the pattern the comparison won't be accurate.
Include the header:
#import <objc/runtime.h>
Create the category:
@interface UIColor(CustomPatterns)
@property (strong, nonatomic) NSString* patternName;
@end
@implementation UIColor(CustomPatterns)
static char CUSTOM_PATTERNS_PATTERN_NAME_KEY;
@dynamic patternName;
-(void)setPatternName:(NSString *)patternName
{
objc_setAssociatedObject(self, &CUSTOM_PATTERNS_PATTERN_NAME_KEY, patternName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)patternName
{
return (NSString*)objc_getAssociatedObject(self, &CUSTOM_PATTERNS_PATTERN_NAME_KEY);
}
@end
And then you can set your custom data and compare:
-(void)comparePatterns
{
UIColor *aColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"1"]];
aColor.patternName = @"1";
UIColor *bColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"1"]];
bColor.patternName = @"1";
UIColor *cColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"2"]];
cColor.patternName = @"2";
NSLog(@"Should be true: %d",[aColor.patternName isEqualToString:bColor.patternName]);
NSLog(@"Should be false: %d",[aColor.patternName isEqualToString:cColor.patternName]);
}
How do I compare 2 UIColor objects which are kCGColorSpaceModelRGB and UIExtendedSRGBColorSpace instances in logs?
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:
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 toCGColor
: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)
}
}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
==
orisEqual
in your text processing.)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.
Related Topics
Xcode 4.2 with Arc: Will My Code Run Even on iOS Devices with Firmware Older Than 5.0
Swift - Add Same Navigation Bar Items to Every Page
How to Make a Reusable Tableview for Different Screens in the Same Application
Notification in Swift Every Day at a Set Time
App Crashes After Updating Coredata Model That Is Being Displayed in a UItableview
How to Programmatically Delete My Own iOS App
How to Programmatically Wrap Png Texture Around Cube in Scenekit
How to Remove "Optional" from String
Invalid Transaction Receipt Returned by Appstorereceipturl (Nsdata), in iOS 7
Google Signin: Unable to Disconnect iOS App
How to Use Sketch UI Elements Directly into Xcode
Update Responsejson to Responsedecodable in Swift
How to Connect Xcode 9 and Github
iOS Framework Does Not Work on Simulator
iOS App Breakpoints on Running
Converting Nsdictionary to Xml
Scenekit Flattenedclone - Incorrect Use of Objc_Storeweak() and Objc_Loadweak() Error