For the charting class I am working on I wanted to modify a given UIColor by darkening it and also by making it more translucent. This is both straightforward for some aspects but also tricky for others as I have learned.
Looking at the documentation for UIColor you find that there are only several methods provided of creating a UIColor, but none to actually modify it. Neither you will find any colorByModifying… methods, nor is there a UIMutableColor class true to the general rule of keeping mutable and immutable variants for all the important classes.
If you do a bit of Quartz drawing then you also know about CGColor, CG being short for CoreGraphics. Here we have more luck. Actually UIColor is basically a wrapper for a CGColor as I have learned subsequently. By using CoreGraphics functions to modify this internal CG color I was able to come up with two category methods extending UIColor with the missing functionality.
UIColor has an appropriately named method CGColor which returns the true keeper of color values for this UIColor instance. Then you can use the CoreGraphics methods to get the color components, modify them to your heart’s content and re-wrap them into a UIColor.
“Why not stick to CGColor althogether?” you ask. Well, you cannot usefully put CGColors into an NSArray because CoreFoundation classes (anything with Core… in the name) are not listening to retain, release and autorelease. So while it’s possible to force a CGColorRef into an (id) and then add this to an NSArray you are setting yourself up for nasty memory leaks. Conversely UIColor gives you all the compatibility with your favorite storage classes like NSArray, NSDictionary and NSSet. So unless you prefer to stick to the C-Style level of CoreCoundation it’s smarter to use this to your advantage.
Why do I know that UIColor is only a wrapper? Experimentation! If you take the CGColor from a UIColor, retrieve the components with CGColorGetComponents and then modify the returned array you find that you actually have modified the original UIColor. I lost an hour over this wondering why my modifications would also modify “the original color”. This is why, and it is also the answer why you don’t need to reserve memory for the return value of CGColor or even the components, because you are simply getting a pointer to the original memory space where these are held.
Armed with this knowledge you can make a new component array, copy over the original and create a new CGColor.
There is another catch. If you like to use standard colors like [UIColor grayColor] then you will find that these are only using 2-element component arrays: white-value and alpha. Whereas [UIColor redColor] uses 4-element component arrays: Red, Green, Blue and alpha. Fortunately you can query CGColorGetNumberOfComponents to learn just how many elements there are and react accordingly. I was searching for an other hour to find the reason why reducing the alpha for a grayColor would make the color yellow.
I present to you one method to set the alpha of a given color to an arbitrary value. The second method darkens the color by 30%.
UIColor+Tools.h
#import @interface UIColor (Tools) - (UIColor *)colorByDarkeningColor; - (UIColor *)colorByChangingAlphaTo:(CGFloat)newAlpha; @end |
UIColor+Tools.m
#import "UIColor+Tools.h" @implementation UIColor (Tools) - (UIColor *)colorByDarkeningColor { // oldComponents is the array INSIDE the original color // changing these changes the original, so we copy it CGFloat *oldComponents = (CGFloat *)CGColorGetComponents([self CGColor]); CGFloat newComponents[4]; int numComponents = CGColorGetNumberOfComponents([self CGColor]); switch (numComponents) { case 2: { //grayscale newComponents[0] = oldComponents[0]*0.7; newComponents[1] = oldComponents[0]*0.7; newComponents[2] = oldComponents[0]*0.7; newComponents[3] = oldComponents[1]; break; } case 4: { //RGBA newComponents[0] = oldComponents[0]*0.7; newComponents[1] = oldComponents[1]*0.7; newComponents[2] = oldComponents[2]*0.7; newComponents[3] = oldComponents[3]; break; } } CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGColorRef newColor = CGColorCreate(colorSpace, newComponents); CGColorSpaceRelease(colorSpace); UIColor *retColor = [UIColor colorWithCGColor:newColor]; CGColorRelease(newColor); return retColor; } - (UIColor *)colorByChangingAlphaTo:(CGFloat)newAlpha; { // oldComponents is the array INSIDE the original color // changing these changes the original, so we copy it CGFloat *oldComponents = (CGFloat *)CGColorGetComponents([self CGColor]); int numComponents = CGColorGetNumberOfComponents([self CGColor]); CGFloat newComponents[4]; switch (numComponents) { case 2: { //grayscale newComponents[0] = oldComponents[0]; newComponents[1] = oldComponents[0]; newComponents[2] = oldComponents[0]; newComponents[3] = newAlpha; break; } case 4: { //RGBA newComponents[0] = oldComponents[0]; newComponents[1] = oldComponents[1]; newComponents[2] = oldComponents[2]; newComponents[3] = newAlpha; break; } } CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGColorRef newColor = CGColorCreate(colorSpace, newComponents); CGColorSpaceRelease(colorSpace); UIColor *retColor = [UIColor colorWithCGColor:newColor]; CGColorRelease(newColor); return retColor; } |
To make it easier on myself I always return a 4-component color. Grayscale colors get converted by copying the white value to all of the RGB components. This method also allows for proper memory management. True to form the returned UIColor is autoreleased.
And because I love to hand out bonusses here is a convenience method which also found a new home in my UIColor+Tools category. Often I need to get a sufficiently unique color for a line of a chart. My friend Christian Pfandler used his color wheel skillz and helped me come up with a list of colors that are spaced evenly over the whole color space so that you get colors which are unique enough for charts. I also modernized this method with what I had learned.
+ (UIColor *)colorForIndex:(NSInteger)idx { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGFloat components[4] = {0, 0, 0, 1}; switch (idx) { case 0: components[0] = 1; break; case 1: components[1] = 0.8; break; case 2: components[2] = 1; break; break; case 3: components[0] = 1; components[1] = 1; break; case 4: components[1] = 1; components[2] = 1; break; case 5: components[0] = 1; components[2] = 1; break; case 6: components[0] = 1; components[1] = 0.5; break; case 7: components[1] = 1; components[2] = 0.5; break; case 8: components[0] = 0.5; components[2] = 1; break; case 9: components[0] = 0.5; components[1] = 1; break; case 10: components[1] = 0.5; components[2] = 1; break; case 11: components[2] = 0.5; break; case 12: components[0] = 1; components[1] = 0.33; components[2] = 0.33; break; case 13: components[0] = 0.33; components[1] = 0.8; components[2] = 0.33; break; case 14: components[0] = 0.33; components[1] = 0.33; components[2] = 1; break; case 15: components[0] = 1; components[1] = 1; components[2] = 0.33; break; case 16: components[0] = 0.33; components[1] = 1; components[2] = 1; break; case 17: components[0] = 1; components[1] = 0.33; components[2] = 1; break; case 18: components[0] = 1; components[1] = 0.66; components[2] = 0.33; break; case 19: components[0] = 0.33; components[1] = 1; components[2] = 0.66; break; case 20: components[0] = 0.66; components[1] = 0.33; components[2] = 1; break; case 21: components[0] = 0.66; components[1] = 1; components[2] = 0.33; break; case 22: components[0] = 0.33; components[1] = 0.66; components[2] = 1; break; case 23: components[0] = 0.33; components[1] = 0.33; components[2] = 0.66; break; } CGColorRef newColor = CGColorCreate(colorSpace, components); CGColorSpaceRelease(colorSpace); UIColor *retColor = [UIColor colorWithCGColor:newColor]; CGColorRelease(newColor); return retColor; // more colors: make each 0 -> 0.33 and each 0.5 a 0.66, Ones stay the same } |
You can see that this is a class method, also called a “factory method” because you can simply call it via the class name and it creates a new UIColor instance for you to use.
I’m interested to hear in which other UIColor extensions you have come up with or would find useful. Comment below.
Categories: Recipes
Brilliantly useful.
Just last night I was looking up the native values of the default colors (like redColor etc.) just so I could set an lapha by re-creating them using colorWithRed….
Thanks, M.
Manipulating UIColor : just what I was looking for. Thank you!
I made a generalization of your darkening method, here it is for anyone interested :
- (UIColor *)colorByDarkeningColorBy:(CGFloat)factor0to1
{
// oldComponents is the array INSIDE the original color
// changing these changes the original, so we copy it
CGFloat *oldComponents = (CGFloat *)CGColorGetComponents([self CGColor]);
CGFloat newComponents[4];
int numComponents = CGColorGetNumberOfComponents([self CGColor]);
switch (numComponents)
{
case 2:
{
//grayscale
newComponents[0] = oldComponents[0]*factor0to1;
newComponents[1] = oldComponents[0]*factor0to1;
newComponents[2] = oldComponents[0]*factor0to1;
newComponents[3] = oldComponents[1];
break;
}
case 4:
{
//RGBA
newComponents[0] = oldComponents[0]*factor0to1;
newComponents[1] = oldComponents[1]*factor0to1;
newComponents[2] = oldComponents[2]*factor0to1;
newComponents[3] = oldComponents[3];
break;
}
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGColorRef newColor = CGColorCreate(colorSpace, newComponents);
CGColorSpaceRelease(colorSpace);
UIColor *retColor = [UIColor colorWithCGColor:newColor];
CGColorRelease(newColor);
return retColor;
}
Good stuff, but… Why not?
– (UIColor *)colorByDarkeningColorBy:(CGFloat)alpha
{
CGColorRef oldColor = CGColorCreateCopyWithAlpha([self CGColor], alpha);
UIColor *newColor = [UIColor colorWithCGColor:oldColor];
CGColorRelease(oldColor);
return newColor;
}
Thank you for this!
To change the alpha component of a color you can use colorWithAlphaComponent:
Hmm, why are you casting away the ‘const’ on CGColorGetComponents if you aren’t modifying the returned array (in the UIColor+Tools.m code)? And clearly that const is there to tell you not to modify the returned array, although that’s obviously not a very strong protection against someone who’s determined to monkey around with framework internals. 🙂