Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

ArtOfWarfare

macrumors G3
Original poster
Nov 26, 2007
9,558
6,058
I spent the past few hours making this and felt that it was too cool to not share.

I'd love to hear feedback about it... I don't doubt that I've made a big mistake in here someplace, possibly regarding the fact that this class never removes itself as an observer.

Anyways, I wanted the ability to let the user pick from a few predefined colors. I want to be able to easily change the colors, and to be able to see the color picked directly in my storyboard.

Here's my header for my picker:

Code:
extern NSString * const OSWPickableColorDidChangeNotification;
extern NSString * const OSWPickableColorGroupIdentifier;
extern NSString * const OSWPickableColorPickedColor;

@interface OSWPickableColorView : UIView

@property id groupIdentifier;
@property UIColor *pickedColor;

@end

And here's the implementation:

Code:
#import "OSWPickableColorView.h"
#import "UIColor+Multiply.h"

NSString * const OSWPickableColorDidChangeNotification = @"OSWPickableColorDidChangeNotification";
NSString * const OSWPickableColorGroupIdentifier = @"OSWPickableColorGroupIdentifier";
NSString * const OSWPickableColorPickedColor = @"OSWPickableColorPickedColor";

@implementation OSWPickableColorView {
    UIColor *_normalColor;
    UIColor *_darkenedColor;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self setup];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup {
    self.groupIdentifier = @(self.tag);
    _normalColor = self.backgroundColor;
    _darkenedColor = [self.backgroundColor multiplyNonAlphaComponentsBy:0.7];
    [[NSNotificationCenter defaultCenter] addObserverForName:OSWPickableColorDidChangeNotification
                                                      object:nil
                                                       queue:nil
                                                  usingBlock:^(NSNotification *notification) {
        if ([notification.userInfo[OSWPickableColorGroupIdentifier] isEqual:_groupIdentifier]) {
            _pickedColor = notification.userInfo[OSWPickableColorPickedColor];
            [self setNeedsDisplay];
        }
    }];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.backgroundColor = _darkenedColor;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    self.backgroundColor = _normalColor;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.backgroundColor = _normalColor;
    if ([self contains:event]) {
        self.pickedColor = _normalColor;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    self.backgroundColor = [self contains:event] ? _darkenedColor : _normalColor;
}

- (BOOL)contains:(UIEvent *)event {
    CGPoint point = [[[event allTouches] anyObject] locationInView:self];
    return [self pointInside:point withEvent:nil];
}

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    if ([_pickedColor isEqualToColor:_normalColor]) {
        [[UIColor blackColor] set];    
        [[UIBezierPath bezierPathWithRect:self.bounds] stroke];
        [[UIColor whiteColor] set];    
        [[UIBezierPath bezierPathWithRect:CGRectInset(self.bounds, 1, 1)] stroke];
    }
}

- (void)setPickedColor:(UIColor *)pickedColor {
    if ([_pickedColor isEqualToColor:pickedColor]) {
        return;
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:OSWPickableColorDidChangeNotification
                                                        object:nil
                                                      userInfo:@{OSWPickableColorGroupIdentifier:_groupIdentifier,
                                                                     OSWPickableColorPickedColor:pickedColor}];
}

@end

In the process, I also wrote a category on UIColor. Here's the header:

Code:
@interface UIColor (Multiply)

- (bool) isEqualToColor:(UIColor *)color;
- (UIColor *) multiplyNonAlphaComponentsBy:(CGFloat)scaler;

@end

And the implementation:

Code:
#import "UIColor+Multiply.h"

@implementation UIColor (Multiply)

- (bool) isEqualToColor:(UIColor *)color {
    return CGColorEqualToColor(self.CGColor, color.CGColor);
}

- (UIColor *)multiplyNonAlphaComponentsBy:(CGFloat)scaler {
	// 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];
    
	switch (CGColorGetNumberOfComponents([self CGColor])) {
		case 2: // WA
			newComponents[0] = oldComponents[0] * scaler;
			newComponents[1] = oldComponents[0] * scaler;
			newComponents[2] = oldComponents[0] * scaler;
			newComponents[3] = oldComponents[1];
			break;
		case 4: // RGBA
			newComponents[0] = oldComponents[0] * scaler;
			newComponents[1] = oldComponents[1] * scaler;
			newComponents[2] = oldComponents[2] * scaler;
			newComponents[3] = oldComponents[3];
			break;
        default:
            NSLog(@"Unhandled case in UIColor+Multiply!");
	}
    
	CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
	CGColorRef newColor = CGColorCreate(colorSpace, newComponents);
	CGColorSpaceRelease(colorSpace);
    
	UIColor *retColor = [UIColor colorWithCGColor:newColor];
	CGColorRelease(newColor);
    
	return retColor;
}

@end

The way you use the class is you just drop in a bunch of UIViews with the background colors that you want the user to be able to pick from. If multiple sets of colors will be present at the same time, you can indicate which group each color belongs to by changing the tag. Then change the class of the UIView to instead be my PickableColorView.

If you want to programmatically set the selected color, use:

Code:
[[NSNotificationCenter defaultCenter] postNotificationName:OSWPickableColorDidChangeNotification
                                                            object:nil
                                                          userInfo:@{OSWPickableColorGroupIdentifier:@(<Tag of the set you want to pick from>),
                                                                         OSWPickableColorPickedColor:<Color you want to pick>}];

If you need to get the selected color from a controller class, add the controller as a listener for OSWPickableColorDidChangeNotification, use the tag (it's in the user dictionary with the key OSWPickableColorGroupIdentifier) and then you'll know the color (it's also in the user dictionary, with the key OSWPickableColorPickedColor). Your controller will need to import OSWPickableColorView, of course.

Alternatively, you can just connect to any of the cells and use the property pickedColor. It doesn't matter which cell you pick, so long as it's in the right group, since they'll use notifications to synchronize the property to be the same across all of them within the group.

I've attached screenshots of an example of my picker (placed in a UITableView). The first shows what it looks like in Xcode's Interface Builder, which I have set to show as iOS 6.1. The second shows what it looks like running in iOS Simulator 7.0.3.
 

Attachments

  • Screen Shot 2014-01-12 at 2.03.19 AM.png
    Screen Shot 2014-01-12 at 2.03.19 AM.png
    12.4 KB · Views: 140
  • Screen Shot 2014-01-12 at 2.03.36 AM.png
    Screen Shot 2014-01-12 at 2.03.36 AM.png
    16.4 KB · Views: 151

blueillusion

macrumors member
Aug 18, 2008
56
3
That's an interesting way of going about it.
I also created a colour picker for my app, along with other attributes.
I used a UICollectionView instead however. And secondly, I did everything in code (I still have yet to make an interface using IB for iOS, on the mac, that's a different story).
The nice thing about the UICollectionView is that you can make the color picker look however you like. I have two layouts for it. Multiline (as in the screenshot below), and single line (when the app is in landscape).

My result of this:
screen568x568.jpeg


However, my picker doesn't allow the user to change colours, or combine them.
 

ArtOfWarfare

macrumors G3
Original poster
Nov 26, 2007
9,558
6,058
That's an interesting way of going about it.
I also created a colour picker for my app, along with other attributes.
I used a UICollectionView instead however. And secondly, I did everything in code (I still have yet to make an interface using IB for iOS, on the mac, that's a different story).
The nice thing about the UICollectionView is that you can make the color picker look however you like. I have two layouts for it. Multiline (as in the screenshot below), and single line (when the app is in landscape).

My result of this:
Image

However, my picker doesn't allow the user to change colours, or combine them.

Since each cell in mine operates on it's own, I can arrange them however I like. I was thinking about arranging them in a wheel or square or something instead. But the two lines of six word well in a table view cell, so that's what I'm sticking with. When I want to change the colors available to pick, all I have to do is change the color of the cell in IB or move them around if I want them positioned differently.
 

Duncan C

macrumors 6502a
Jan 21, 2008
853
0
Northern Virginia
Since each cell in mine operates on it's own, I can arrange them however I like. I was thinking about arranging them in a wheel or square or something instead. But the two lines of six word well in a table view cell, so that's what I'm sticking with. When I want to change the colors available to pick, all I have to do is change the color of the cell in IB or move them around if I want them positioned differently.

Looks pretty cool. If you want different geometry for your tiles you might look at UICollectionView. It's a lot more flexible than a table view.

Beware that failing to remove an object as an observer will cause crashes since after the object that's observing gets deallocated, the notification gets sent to an invalid address. This is a type of zombie that can still occur in ARC.

Also if you display this picker, close it, and display it again without deallocating it, make sure you don't add yourself as an observer twice. If you do you'll get 2 notifications per event (3 if you add yourself 3 times, etc.) And you'll need to remove yourself 3 times, or use the removeAllObservers call.
 

ArtOfWarfare

macrumors G3
Original poster
Nov 26, 2007
9,558
6,058
Looks pretty cool. If you want different geometry for your tiles you might look at UICollectionView. It's a lot more flexible than a table view.

Beware that failing to remove an object as an observer will cause crashes since after the object that's observing gets deallocated, the notification gets sent to an invalid address. This is a type of zombie that can still occur in ARC.

Also if you display this picker, close it, and display it again without deallocating it, make sure you don't add yourself as an observer twice. If you do you'll get 2 notifications per event (3 if you add yourself 3 times, etc.) And you'll need to remove yourself 3 times, or use the removeAllObservers call.

I wasn't sure how to handle removing the observer. Should I implement a dealloc method where I remove it as an observer? Or is there a way a UIView can know when it's been removed from the screen?

Also, my color picker depends on a table view in no way. I just put them in a table view because I decided that was most aesthetically pleasing / seemed like a normal thing for an iOS app. I originally was using just a blank view, but I decided it didn't seem to give emphasis to the different parts of the screen properly.
 

Duncan C

macrumors 6502a
Jan 21, 2008
853
0
Northern Virginia
I wasn't sure how to handle removing the observer. Should I implement a dealloc method where I remove it as an observer? Or is there a way a UIView can know when it's been removed from the screen?

Also, my color picker depends on a table view in no way. I just put them in a table view because I decided that was most aesthetically pleasing / seemed like a normal thing for an iOS app. I originally was using just a blank view, but I decided it didn't seem to give emphasis to the different parts of the screen properly.

Yes, implementing dealloc and putting a remove observer call there would be good.

I don't know if it would work to override removeFromSuperview and put it there.

I use the block form of notification handler: addObserverForName:eek:bject:queue:usingBlock: and save a pointer to the notification into an instance variable. Then when I remove the observer I set the instance variable to nil. That way there's no danger in removing an observer more than once.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.