CALayer - remove from view - ios

I'm adding CAShapeLayer with some CABasicAnimation. I want to be able to remove the layer and draw it again but I can't succeed with this.
- (void)drawRect:(CGRect)rect {
[self drawLayer];
}
- (void)drawLayer {
if (_alayer && _layer.superlayer) {
[_alayer removeFromSuperlayer];
_alayer = nil;
}
_alayer = [[CAShapeLayer alloc] init];
_alayer.path = [self myPath].CGPath;
_alayer.strokeColor = [UIColor redColor].CGColor;
_alayer.fillColor = [UIColor clearColor].CGColor;
_alayer.lineWidth = 2.f;
_alayer.strokeStart = 0.f;
_alayer.strokeEnd = 1.f;
[self.layer addSublayer:_alayer];
// animate
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 2.f;
animation.fromValue = #0.f;
animation.toValue = #1.f;
[_alayer addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
However a moment after calling [self setNeedsDisplay]; even though it stops at the breakpoint in drawLayer method, the drawing doesn't disappear and animate in again. alayer is declared as nonatomic, strong property. What am I doing wrong?

You should not call [self drawLayer]; fromm drawRect: because it is called periodically.
Use other viewDidLoad or call it from other method instead
Edited:
drawRect: should only be used for drawing, I don't know if it will even work if you add animation from there.
If you want it to disappear and appear, you should try to add delay using [self performSelector:withObject:afterDelay:].
I think changing hidden or opacity should suffice, you only need to removeAllAnimations to make sure no more animation attached to that layer.
However I think you should try other way than calling drawLayer from drawRect: because I don't it its good practice anyway.

The UIView has its own property layer by default. Please try to change a variable name to shapeLayer or another one.

Related

Why addAnimation:forKey not working in viewDidLoaded

I have the following method called by viewDidLoad. This method is to create a CALayer to show a image. This layer has a mask whose path is a UIBezierPath created by my private method. I want the mask to rotate infinitely, and then I add a CABasicAnimation object to the mask.
- (void) createPathLmask
{
// mask layer
self.pathLayer = [CALayer layer];
self.pathLayer.bounds = CGRectMake(0.0, 0.0, 120, 120);
CGPoint position = self.view.layer.position;
position.y += 140;
self.pathLayer.position = position;
self.pathLayer.backgroundColor = [UIColor redColor].CGColor;
UIImage *backimage = [UIImage imageNamed:#"image2"];
self.pathLayer.contents = (__bridge id)backimage.CGImage;
self.pathLayer.contentsGravity = kCAGravityResizeAspectFill;
// mask
CAShapeLayer *mask = [CAShapeLayer layer];
mask.bounds = CGRectMake(0.0, 0.0, 120, 120);
mask.position = CGPointMake(60.0f, 60.0f);
mask.contentsGravity = kCAGravityResizeAspectFill;
mask.path = [self createBezierPathInRect:mask.bounds].CGPath;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// rotate the mask repeatedly
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = #"transform.rotation";
animation.duration = 4.0f;
animation.byValue = #(M_PI * 2);
animation.repeatCount = HUGE_VALF;
[mask addAnimation:animation forKey:#"rotation_repeatedly"];
});
self.pathLayer.mask = mask;
[self.view.layer addSublayer:self.pathLayer ];
}
I find that the rotation animation can only work when I put the addAnimation:forKey into the dispatch_after block with 1 second delay. If those codes are moved out of the block, the mask will not rotate.
So there must be something not ready when animation is added to the layer in viewDidLoaded. I am wondering what is not ready yet? Is there any document or explanation about the suitable chance to add the animation?
So there must be something not ready when animation is added to the layer in viewDidLoaded
Correct. This is way too early for animation. Keep in mind that the view at this point merely exists and that's all; it isn't even part of the interface. You cannot animate a view that isn't part of the view hierarchy. There is nothing, at this point, to animate.
The view first becomes part of the interface between the first call to viewWillAppear and the first call to viewDidAppear. That is what "appear" means (as opposed to what "loaded") means.
There's great documentation on Apple's site here. Putting in simply though:
ViewWillAppear - This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view. You can override this method to perform custom tasks associated with displaying the view. For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented.

How do I create a growing iOS Button?

My friend gave me the following designs for iOS buttons, and I'm not sure the best way to implement this.
I need to make the reusable button shown below (in Objective-C).
I've tried:
Subclassing the button, but read that I shouldn't do that
Animating the border (while subclassed), but the border only goes inwards, so it seems like I need to animate the frame too
So how do I approach this? I'm assuming making a CustomButtonView class which has a button (composition) as well as an inner and outer circle view? How would I then animate that to grow? Would I have to animate the frame change too, or could I use insets?
What is the simplest code to make this work? Thanks!
Here Is the approach I took to create this:
Subclass UIView to create your custom button
Use UITapGestureRecognizer or touchesBegan, touchesEnded... for your interaction
Add two CALayer's for your foreground and background layers
Add your icon layer (This can be an UIImageView or any other way of displaying an image)
- (id)initWithIcon:(UIImage *)icon backgroundColor:(UIColor *)backgroundColor foregroundColor:(UIColor *)foregroundColor {
if (self = [super init]) {
// Background Layer Setup
_backgroundLayer = [CALayer new];
[_backgroundLayer setBackgroundColor:backgroundColor.CGColor];
[self.layer addSublayer:_backgroundLayer];
// Foreground Layer Setup
_foregroundLayer = [CALayer new];
[_foregroundLayer setBackgroundColor:foregroundColor.CGColor];
[self.layer addSublayer:_foregroundLayer];
// Icon Setup
_icon = [[UIImageView alloc] initWithImage:icon];
[_icon setContentMode:UIViewContentModeCenter];
[self addSubview:_icon];
UIGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(buttonTapped:)];
[self addGestureRecognizer:tapGesture];
}
return self;
}
- (void)setFrame:(CGRect)frame {
// Make sure super is called
[super setFrame:frame];
// Build the layout of backgroundLayer
[self.backgroundLayer setFrame:CGRectMake(frame.size.width*0.1, frame.size.width*0.1, frame.size.width*0.8, frame.size.width*0.8)];
[self.backgroundLayer setCornerRadius:frame.size.width*0.8/2];
// Build the layout of forgroundLayer
[self.foregroundLayer setFrame:CGRectMake(frame.size.width*0.05, frame.size.width*0.05, frame.size.width*0.9, frame.size.width*0.9)];
[self.foregroundLayer setCornerRadius:frame.size.width*0.9/2];
// Build the frame of your icon
[self.icon setFrame:CGRectMake(0, 0, frame.size.width, frame.size.width)];
}
- (void)buttonTapped:(UIGestureRecognizer*)gesture {
// Animate the foreground getting smaller
CABasicAnimation *foregroundFrameChange = [CABasicAnimation animationWithKeyPath:#"frame"];
foregroundFrameChange.fromValue = [NSValue valueWithCGRect:_foregroundLayer.frame];
foregroundFrameChange.toValue = [NSValue valueWithCGRect:CGRectMake(self.frame.size.width*0.1,self.frame.size.width*0.1, self.frame.size.width*0.8, self.frame.size.width*0.8)];
self.foregroundLayer.frame = CGRectMake(self.frame.size.width*0.1,self.frame.size.width*0.1, self.frame.size.width*0.8, self.frame.size.width*0.8);
// Animate the forground cornerRadius to stay rounded
CABasicAnimation *foregroundRadiusChange = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
foregroundRadiusChange.fromValue = [NSNumber numberWithDouble:self.foregroundLayer.cornerRadius];
foregroundRadiusChange.toValue = [NSNumber numberWithDouble:self.frame.size.width*0.8/2];
[self.foregroundLayer setCornerRadius:self.frame.size.width*0.8/2];
// Animate the background getting larger
CABasicAnimation *backgroundFrameChange = [CABasicAnimation animationWithKeyPath:#"frame"];
backgroundFrameChange.fromValue = [NSValue valueWithCGRect:self.backgroundLayer.frame];
backgroundFrameChange.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.width)];
self.backgroundLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.width);
// Animate the background cornerRadius to stay rounded
CABasicAnimation *backgroundRadiusChange = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
backgroundRadiusChange.fromValue = [NSNumber numberWithDouble:self.backgroundLayer.cornerRadius];
backgroundRadiusChange.toValue = [NSNumber numberWithDouble:self.frame.size.width/2];
[self.backgroundLayer setCornerRadius:self.frame.size.width/2];
// Group all the animations to run simultaneously
CAAnimationGroup *allAnimations = [CAAnimationGroup animation];
allAnimations.duration = 2;
allAnimations.animations = #[foregroundFrameChange, foregroundRadiusChange, backgroundFrameChange, backgroundRadiusChange];
allAnimations.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.layer addAnimation:allAnimations forKey:#"animate"];
// Create your button action callback here
}
This was a quick mock up and not a complete solution but it will give you something to play with.

CAAnimation Delegate not called Custom Loading Animation

I'm trying to create a custom loading animation. The animation consists of two parts. First, 5 lines are drawn (animated) outwards. When the lines are finished drawing, they are faded away and the cycle is started over. In order to get this sequential behavior with two animations, I am using two CABasicAnimations, both of which have the same delegate. In the delegate method animationDidStop:, I dispatch to the correct animation (animation 2 if animation 1 just finished, and vise versa). Here is the relevant code:
-(void)startAnimating{//this method launches the line growing animation
self.hidden = NO;
BOOL delegateSet = NO;
animationPhase = 1;
for(CAShapeLayer * pathLayer in _lines){
[pathLayer removeAllAnimations];
pathLayer.strokeColor = _strokeColor.CGColor;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = .95;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
if(!delegateSet){//just set one delegate
pathAnimation.delegate = self;
delegateSet = YES;
}
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
}
//this delegate method dispatches the next animation
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if(animationPhase == 1)[self fadeLines];
else if(animationPhase == 2)[self startAnimating];
}
-(void)fadeLines{//this method launches the fading animation
self.hidden = NO;
BOOL delegateSet = NO;
animationPhase = 2;
for(CAShapeLayer * pathLayer in _lines){
[pathLayer removeAllAnimations];
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) pathLayer.strokeColor;
strokeAnim.toValue = (id) [UIColor clearColor].CGColor;
strokeAnim.duration = .2;
if(!delegateSet){//just set one delegate
strokeAnim.delegate = self;
delegateSet = YES;
}
[pathLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
pathLayer.strokeColor = [UIColor clearColor].CGColor;
}
}
So this all works great when it's run on it's own (not while other things are being done in the background). But when I use it as a loading animation (while other things are being done in the background), the first initial grow animation is performed, but animationDidStop: is never called.
I've tried using transactions with completion blocks instead of using the delegate pattern, but I get the same issue. I can't use CAAnimationGroup with appropriate beginTimes because I need the animations to loop.
Another thing to note is if I just perform the growing animation with a high repeat count, everything works fine. The only issue is the sequential looping of the two animations. If anyone knows how I can fix this, or knows a different way to sequentially loop two CAAnimations, I will be very grateful.

CAShapeLayer not Animated

I am having trouble figuring out how to get animation working. I have a UIView called BallView . I can draw a large red circle fine. I want that circle to fade from red to green. I setup a CAShapeLayer and CABasicAnimation in my view's initializer but the animation doesn't work. Here is my initializer:
- (void)initHelper:(UIColor*)c {
CAShapeLayer* sl = (CAShapeLayer*) self.layer;
sl.fillColor = [UIColor redColor].CGColor;
sl.path = [[UIBezierPath bezierPathWithOvalInRect:self.bounds] CGPath];
CABasicAnimation *colorAnim = [CABasicAnimation animationWithKeyPath:#"fillColor"];
colorAnim.duration = 5.0;
colorAnim.fromValue = [UIColor redColor];
colorAnim.toValue = [UIColor greenColor];
colorAnim.repeatCount = 10;
colorAnim.autoreverses = YES;
[sl addAnimation:colorAnim forKey:#"fillColor"];
}
Your from and to values are not right, you should set
colorAnim.fromValue = (__bridge id)[UIColor redColor].CGColor;
colorAnim.toValue = (__bridge id)[UIColor greenColor].CGColor;
You shouldn't have put animations in the init method, because at that time this view is being added to the view hierarchy. Try to move the initHelper method out of init method and expose it in BallView.h, thus you can call it whenever you want in the view Controller, you can add it in viewDidAppear method. If you don't want to do that,you can simply delay the fire time of the initHelper like this:
[self performSelector:#selector(initHelper:) withObject:nil afterDelay:2.0];

Combining drawRect with KeyPathAnimation to undraw flickers

I am drawing a path in my UIView's drawRect method. In certain situations that path will go away and I want to animate this - let's call it "undrawing" of the path.
For the animation, I am creating a CAShapeLayer, give it myPath and then set up a CABasicAnimation (animationWithKeyPath:#"strokeEnd").
My problem is how to switch from what is drawn in drawRect to the animation. I have to remove myPath from what is drawn from drawRect and call setNeedsDisplay - otherwise the animation would be hidden by the path drawn from drawRect.
- (void)animationDidStart:(CAAnimation *)theAnimation
{
myPath = nil;
[self setNeedsDisplay];
}
But this way I see my path, then a quick flickering with no path, then the path is rendered by CoreAnimation again and is nicely undrawn.
Can I do better to avoid the flickering?
Background
This is how I set up the animation:
- (void) startAnimation
{
pathLayer.path = myPath.CGPath;
pathLayer.hidden = false;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1;
pathAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.delegate = self;
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
I believe the cause of the flickering is the implicit animation that runs when you call
pathLayer.hidden = NO;
By default, standalone CALayers animate most property changes implicitly. If you disable the implicit animation for the hidden property, it should run without flickering (regardless whether you use drawRect: or drawLayer:inContext:):
[CATransaction begin];
[CATransaction setDisableActions:YES];
pathLayer.hidden = NO;
[CATransaction commit];
Here is my solution: Don't combine UIKits drawRect with CAAnimation but draw into a CALayer instead:
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext
This allows flicker-free switching from drawing to animation.

Resources