I'm trying to make a button that will show/hide a color-selection slider. That's the easy part.
What I am not sure how to do is to make the button be a circle with an outline (say black) and the fill be the color shown in the slider. As the user moves the slider, the color of the circle should change accordingly.
I am new to iOS development, so probably I am approaching this all wrong. I would really appreciate any ideas even they are completely different from what I have here.
I started to approach this with a subclass using QuartzCore layers, and the result is decent, but there are issues/things that bother me:
For some reason I get no highlight state when pressing the button
It's obviously odd to use rounded corners to achieve a circle
Unfortunately, it seems at the point were I draw the layers, the button is not laid out yet so I have had to hardcode the radius instead of making it based on the button size.
So, yeah I hardly think this approach is ideal. I will greatly appreciate any and all help. Thank you!
#import "ColorPickerButton.h"
#implementation ColorPickerButton
#pragma mark - UIButton Overrides
+ (ColorPickerButton *)buttonWithType:(UIButtonType)buttonType
{
return [super buttonWithType:UIButtonTypeCustom];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
r = 0.3;
g = 0.6;
b = 0.9;
[self drawOutline];
[self drawColor];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)layoutSubviews
{
_outlineLayer.frame = CGRectInset(self.bounds, 5, 5);
_colorLayer.frame = CGRectInset(self.bounds, 5+_outlineLayer.borderWidth, 5+_outlineLayer.borderWidth);
[super layoutSubviews];
}
#pragma mark - Layer setters
- (void)drawOutline
{
if (!_outlineLayer)
{
_outlineLayer = [CALayer layer];
_outlineLayer.cornerRadius = 20.0;
_outlineLayer.borderWidth = 3;
_outlineLayer.borderColor = [[UIColor colorWithRed:170.0/255.0 green:170.0/255.0 blue:170.0/255.0 alpha:1.0] CGColor];
_outlineLayer.opacity = 0.6;
[self.layer insertSublayer:_outlineLayer atIndex:1];
}
}
- (void)drawColor
{
if (!_colorLayer)
{
_colorLayer = [CALayer layer];
}
_colorLayer.cornerRadius = 16.0;
_colorLayer.backgroundColor = [[UIColor colorWithRed:r green:g blue:b alpha:1.0] CGColor];
_colorLayer.opacity = 1.0;
[self.layer insertSublayer:_colorLayer atIndex:2];
}
- (void)changeColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue
{
r = red;
g = green;
b = blue;
[self drawColor];
}
#end
We do this in our app. We use the CALayer on the button and add rounded corners 1/2 the size of the component width. We add a border to the button and then set the button background color to achieve the 'fill' color. Works well.
Related
I have created a method that I would like to call on UITextField. Here is the method:
- (void)addBorderLayer:(UITextField *)tf{
CALayer *border = [CALayer layer];
CGFloat borderWidth = 2;
border.borderColor = [UIColor redColor].CGColor;
border.frame = CGRectMake(0, tf.frame.size.height - borderWidth, tf.frame.size.width, tf.frame.size.height);
border.borderWidth = borderWidth;
tf.layer.masksToBounds = YES;
}
Once I attempt to call the method it doesn't change my UITextField:
- (void)viewDidLoad {
[super viewDidLoad];
[self addBorderLayer:self.emailTextField];
}
What am I doing wrong?
In addBorderLayer: you need to add the layer to text field after you set it up:
[tf.layer addSublayer:border];
You should change the layer of your textfield. Consider something like this:
-(void) addBorderLayer:(UITextField *) tf
{
tf.layer.borderColor = [UIColor redColor].CGColor;
tf.layer.borderWidth = 2;
tf.layer.cornerRadius = 5;
}
The issue is as everyone else has said you aren't actually adding the layer to the UITextField. The other answers are all good but I thought I'd share a something that might seem a little more complicated but would probably make it more reusable and that's always good.
First thing is create a new category for UIView called UIView+layer and in here we'd have the following:
UIView+layer.h
#interface UIView(layer)
- (void)addBorder;
- (void)addBorderWithColor:(UIColor *)color;
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width;
- (void)addBorderWithColor:(UIColor *)color radius:(CGFloat)radius;
- (void)addBorderWithWidth:(CGFloat)width;
- (void)addBorderWithWidth:(CGFloat)width radius:(CGFloat)radius;
- (void)addBorderWithRadius:(CGFloat)radius;
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width radius:(CGFloat)radius; // Essentially the one we will use
#end
UIView+layer.m
#import "UIView+layer.h"
#pragma mark - Default values
// Some constants for the default values
CGFloat const kDefaultBorderWidth = 2.0;
CGFloat const kDefaultBorderRadius = 5.0;
UIColor * const kDefaultBorderColor = [UIColor blackColor];
#implementation UIView(layer)
// Essentially this is the method all the others will call.
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width radius:(CGFloat)radius
{
CALayer *border = [CALayer layer];
border.borderColor = color.CGColor;
border.frame = CGRectMake(0, self.frame.size.height - width, self.frame.size.width, self.frame.size.height);
border.borderWidth = width;
self.layer.masksToBounds = YES;
[self addSublayer:layer];
// OR you can affect the layer it already has like with the code below instead of using the addSublayer: method
// self.layer.borderColor = color.CGColor;
// self.layer.borderWidth = width;
// self.layer.frame = CGRectMake(0, self.frame.size.height - width, self.frame.size.width, self.frame.size.height);
}
#pragma mark - additional methods that you can call for setting a border
- (void)addBorder
{
[self addBorderWithColor:kDefaultBorderColor
width:kDefaultBorderWidth
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color
{
[self addBorderWithColor:color
width:kDefaultBorderWidth
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width
{
[self addBorderWithColor:color
width:width
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color radius:(CGFloat)radius
{
[self addBorderWithColor:color
width:kDefaultBorderWidth
radius:radius];
}
- (void)addBorderWithWidth:(CGFloat)width
{
[self addBorderWithColor:kDefaultBorderColor
width:width
radius:kDefaultBorderRadius];
}
- (void)addBorderWithWidth:(CGFloat)width radius:(CGFloat)radius
{
[self addBorderWithColor:kDefaultBorderColor
width:width
radius:radius];
}
- (void)addBorderWithRadius:(CGFloat)radius
{
[self addBorderWithColor:kDefaultBorderColor
width:kDefaultBorderWidth
radius:radius];
}
#end
Now if you don't know anything about categories you're probably wondering why we did UIView instead of UITextField. The reason being is that we are making it reusable with all classes that are a subclass of UIView and that includes UITextField so all you need to do is do #import "UIView+layer.h" and then you can use these methods for the instance of your UITextField so in your case you can now just do [self.emailTextField addBorder]; and it will add a border to your UITextField using the default values.
I know it looks like a lot more work compared to the others but this just makes it so it is more reusable across more classes that subclass UIView and re-usability is always a code thing when it comes to coding.
Try moving the code to viewWillAppear, at which point it should have had its views positioned and the bounds etc will be correct.
Also add the layer to the text field as pointed out by Greg.
You are creating the new layer, but you never add it to the ui.
You will want to add it to the target view's layer.
[tf.layer addSublayer:border];
This is a follow-up question to How to synchronize CALayer and UIView animations up and down a complex hierarchy
Lets say I have a composite layer (Top) that is a subclass of CALayer and has any number of children. Top has 2 child layers within it. The first sublayer (A) should always be a fixed width - lets say 100 pixels wide. The second sublayer (B) should be the remainder of the size of Top. Both A and B should occupy the entire height of Top. This is pretty straightforward to code up in layoutSubviews.
Let's presume that Top has no knowledge of A or B. Also presume that Top has a delegate that controls when it should be animated (the delegate provides actionForLayer:forKey: and no other CALayer delegate functions).
I'd like to devise a strategy where for every possible size of Top, the user will always see A and B rendered according to the constraints listed above - even when the size of Top is being animated, even when it is being animated with any variety of animation parameters (durations, functions, offsets, etc).
Just as Top's animations are driven from some containing view or layer through its delegate - it seems that A and B should have their animations setup their containing layer - Top. I want to keep things well-composed, so I don't want the layout of A & B within Top to need to be understood by anything other than Top.
So - the question is what's the best strategy to chain the animations down the layer tree to keep all of the animation parameters in sync?
Here's some sample code that does chaining through the use of actionForLayer:forKey:, but middle function has to go through some fairly involved work (which isn't included) to translate all of the settings from its animation to the sublayer's animation. Not included in this sample is any code that deals with interpolating the values of the bounds. For example, imagine a case where an animation is setup to use a different fromValue, or a keyframe animation. Those values would need to be solved for the sublayers and applied accordingly.
#import "ViewController.h"
#interface MyTopLayer : CALayer
#end
static const CGFloat fixedWidth = 100.0;
#implementation MyTopLayer
-(instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [[UIColor redColor] CGColor];
CALayer *fixedLayer = [[CALayer alloc] init];
CALayer *slackLayer = [[CALayer alloc] init];
[self addSublayer:fixedLayer];
[self addSublayer:slackLayer];
fixedLayer.anchorPoint = CGPointMake(0,0);
fixedLayer.position = CGPointMake(0,0);
slackLayer.anchorPoint = CGPointMake(0,0);
slackLayer.position = CGPointMake(fixedWidth,0);
fixedLayer.backgroundColor = [[UIColor yellowColor] CGColor];
slackLayer.backgroundColor = [[UIColor purpleColor] CGColor];
//fixedLayer.delegate = self; // no reason to ever animate this layer since it is static
slackLayer.delegate = self;
}
return self;
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"bounds"]) {
return nil;
}
CAAnimation *boundsAnim = [self animationForKey:#"bounds"];
NSLog(#"boundsAnim=%#", boundsAnim);
if (!boundsAnim) {
return (id<CAAction>)[NSNull null];
}
CAAnimation *sublayerBoundsAnim;
if ([boundsAnim isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *subAnim = [CABasicAnimation animationWithKeyPath:#"bounds"];
// transform properties, like from, to & by value from boundsAnim (outer) to the inner layer's animation
sublayerBoundsAnim = subAnim;
} else {
CAKeyframeAnimation *subAnim = [CAKeyframeAnimation animationWithKeyPath:#"bounds"];
// copy/interpolate keyframes
sublayerBoundsAnim = subAnim;
}
sublayerBoundsAnim.timeOffset = boundsAnim.timeOffset;
sublayerBoundsAnim.duration = boundsAnim.duration;
sublayerBoundsAnim.timingFunction = boundsAnim.timingFunction;
return sublayerBoundsAnim;
}
-(void)layoutSublayers {
{
CALayer *fixedLayer = [self.sublayers firstObject];
CGRect b = self.bounds;
b.size.width = fixedWidth;
fixedLayer.bounds = b;
}
{
CALayer *slackLayer = [self.sublayers lastObject];
CGRect b = self.bounds;
b.size.width -= fixedWidth;
slackLayer.bounds = b;
}
}
#end
#interface MyView : UIView
#end
#implementation MyView
{
bool _shouldAnimate;
}
+(Class)layerClass {
return [MyTopLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.delegate = self;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(doubleTapRecognizer:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapRecognizer:)];
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
}
return self;
}
CGFloat getRandWidth() {
const static int maxWidth=1024;
const static int minWidth=fixedWidth*1.1;
return minWidth+((((CGFloat)rand())/(CGFloat)RAND_MAX)*(maxWidth-minWidth));
}
-(void)tapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = true;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(void)doubleTapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = false;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (_shouldAnimate) {
if ([event isEqualToString:#"bounds"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 2.0;
//anim.timeOffset = 0.5;
anim.fromValue = [NSValue valueWithCGRect:CGRectMake(0,0,100,100)];
return anim;
} else {
return nil;
}
} else {
return (id<CAAction>)[NSNull null];
}
}
#end
My question is - does anybody have a better way to get this done? It seems a little bit scary that I've not seen any mention of this sort of hierarchical chaining anywhere. I'm aware that I would probably also need to do some more work on canceling sublayer animations when the top layer's animation is canceled. Relying simply on the currently attached animation, especially w/out concern for the current time that that function is in seems like it could be a source of errors somewhere down the line.
I'm also not sure how well this would perform in the wild since they aren't in the same animation group. Any thoughts there would be greatly appreciated.
I’m changing the background colour of a UIButton via this category method, using a 1px by 1px image:
- (void)setBackgroundColor:(UIColor *)backgroundColor forState:(UIControlState)state
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0);
[backgroundColor setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 1, 1));
UIImage *backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
[self setBackgroundImage:backgroundImage forState:state];
UIGraphicsEndImageContext();
}
However, this overrides my setting of the .layer.cornerRadius. I need a button with rounded corners, but also one whose background colour I can change on highlighted.
Any way around this? The corner radius needs to be dynamic.
So, all I had to do was ensure that button.layer.masksToBounds was turned on. Problem solved, no subclassing required.
Subclass the UIButton. In your subclass, set the corner radius in the init method. If you're using a xib, this will be initWithDecoder:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.layer.cornerRadius = 5.f;
}
return self;
}
Also subclass the setHighlighted: method. This is where you'll set the background color. Check the "highlighted" value and assign the background color appropriately. In this example, the button is a blue button that is red on highlight with rounded corners. You will need to set the initial color in the nib.
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
self.backgroundColor = (highlighted) ? [UIColor redColor] : [UIColor blueColor];
}
Is it possible to create such a UIView fill with color, but in the middle is transparent?
I'm thinking about to create 5 UIViews here. Just wondering is it possible to accomplish by using only ONE UIView
From Duncan C, I get to know where should I start, then I found CALayer with transparent hole in it.
UIBezierPath *overlayPath = [UIBezierPath bezierPathWithRect:self.view.bounds];
UIBezierPath *transparentPath = [UIBezierPath bezierPathWithRect:CGRectMake(60, 120, 200, 200)];
[overlayPath appendPath:transparentPath];
[overlayPath setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = overlayPath.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor colorWithRed:255/255.0 green:20/255.0 blue:147/255.0 alpha:1].CGColor;
[self.view.layer addSublayer:fillLayer];
Make use of 2 UIBezierPath, then fill with color that I want (in my question is pink color), then add as sublayer
You create a view as subclass of UIView and add these codes:
In YourView.h
#import <UIKit/UIKit.h>
#interface YourView : UIView
#property (nonatomic, assign) CGRect rectForClearing;
#property (nonatomic, strong) UIColor *overallColor;
#end
In YourView.m
#import "YourView.h"
#implementation NBAMiddleTransparentView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder // support init from nib
{
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
[super drawRect:rect];
CGContextRef ct = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(ct, self.overallColor.CGColor);
CGContextFillRect(ct, self.bounds);
CGContextClearRect(ct, self.rectForClearing);
}
#end
Using:
yourView.overallColor = [UIColor redColor];
yourView.rectForClearing = CGRectMake(20, 20, 20, 20);
Hope this helps!
Yes it's possible. You could attach a mask layer to your view's layer, and "punch out" the center portion of the mask. It would actually be quite easy.
I've created a custom button class to be used in my xibs that is basically just a button with a shadow with a label over it. However, the text in the label appears jagged (as though it is not being anti-aliased). Here's my code for the relevant part of the class (it's a very small class that inherits from UIButton).
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self internalInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self internalInit];
}
return self;
}
- (void)internalInit {
self.backgroundColor = [UIColor colorWithRed:22/255.0 green:72/255.0 blue:143/255.0 alpha:1.0];
CGRect frame = self.frame;
frame.origin = CGPointMake(floorf(frame.origin.x), floorf(frame.origin.y));
//self.frame = CGRectIntegral(frame);
frame = self.titleLabel.frame;
frame.origin = CGPointMake(floorf(frame.origin.x), floorf(frame.origin.y));
//self.titleLabel.frame = CGRectIntegral(frame);
// Shadow
self.layer.shadowOffset = CGSizeMake(0, 1.5);
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOpacity = 0.3;
self.layer.shouldRasterize = YES;
self.layer.shadowPath = [[UIBezierPath bezierPathWithRect:self.bounds] CGPath];
// Corner
self.layer.cornerRadius = 5;
}
I've tried troubleshooting the issue and I've found that this can occur when the origin for the label or the button is set at a non-integer value. However, I've checked the absolute value for both the button and the pixel and they are both set to integer values. I haven't been able to figure out what else could be going wrong and I cannot find any others who have had the same issue.
Generally when jaggies happen it's because the same view is being drawn multiple times over itself. Did you confirm this view is only being drawn once?