Circular loading animation (like Weather Channel App) - ios

How would I implement a loading animation like the one found in the Weather Channel iOS App?
Specifically, I have a rounded UIButton that I want a spinning circle around when the user has tapped it, and something is loading.
Preferably, I'd want to make this using a built-in iOS framework, and as few 3rd party libraries as possible.
Here's how the Weather Channel Application looks like (couldn't get a gif, so to see it in action download the application from the App Store):
It doesn't necessarily have to look exactly like this, a solid color would be a good start. But the concept should be the same. I don't believe it should be hard to make, but sadly I have no idea where to start.
Thanks for all help!
EDIT 1:
I should point out that a solid color is good enough, and it doesn't need a gradient or a glow.
I have been working on some simple code that might be in the right direction. Please feel free to use that as a starting point:
- (void)drawRect:(CGRect)rect {
// Make the button round
self.layer.cornerRadius = self.frame.size.width / 2.0;
self.clipsToBounds = YES;
// Create the arc
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2) radius:(self.frame.size.width / 2) startAngle:M_PI_2 endAngle:M_PI clockwise:NO].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor redColor].CGColor;
circle.lineWidth = 10;
// Create the animation
// Add arc to button
[self.layer addSublayer:circle];
}

As pointed out elsewhere, you can just add a view, and the rotate it. If you want to rotate with block-based animation, it might look like:
- (void)rotateView:(UIView *)view {
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
[UIView animateKeyframesWithDuration:1.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat animations:^{
for (NSInteger i = 0; i < 4; i++) {
[UIView addKeyframeWithRelativeStartTime:(CGFloat) i / 4.0 relativeDuration:0.25 animations:^{
view.transform = CGAffineTransformMakeRotation((CGFloat) (i + 1) * M_PI / 2.0);
}];
}
} completion:nil];
} completion:nil];
}
Frankly, while I generally prefer block-based animation, the silliness of wrapping an animation with an animation (which is necessary to get no ease-in/ease-out) makes the block-based approach less compelling. But it illustrates the idea.
Alternatively, in iOS 7 and later, you could use UIKit Dynamics to spin it:
Define a property for the animator:
#property (nonatomic, strong) UIDynamicAnimator *animator;
Instantiate the animator:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
Add rotation to an item:
UIDynamicItemBehavior *rotationBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[self.imageView]];
[rotationBehavior addAngularVelocity:2.0 forItem:self.imageView];
[self.animator addBehavior:rotationBehavior];

Maybe I miss something but I think you only need to rotate a custom spin asset.
You can easily achieve this with a method like:
- (void) runSpinAnimationOnView:(UIView*)view duration:(CGFloat)duration repeat:(float)repeat;
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = repeat;
[view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
You need to repeat it indefinitely. So put HUGE_VALF as repeat parameter.
And if you want to create a custom spin image according to your button size, you can use bezier path. For example, you can do something like:
-(UIImageView*)drawBezierPathInView:(UIView*)view{
UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width, view.frame.size.height));
//this gets the graphic context
CGContextRef context = UIGraphicsGetCurrentContext();
//you can stroke and/or fill
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.5f green:0.5f blue:0.5f alpha:1.f].CGColor);
CGFloat lineWidth = 8.f;
UIBezierPath *spin = [UIBezierPath bezierPath];
[spin addArcWithCenter:CGPointMake(view.frame.size.width * 0.5f, view.frame.size.height * 0.5f) radius:view.frame.size.width * 0.5f - lineWidth * 0.5f startAngle:-M_PI_2 endAngle:2 * M_PI_2 clockwise:YES];
[spin setLineWidth:lineWidth];
[spin stroke];
//now get the image from the context
UIImage *bezierImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *bezierImageView = [[UIImageView alloc]initWithImage:bezierImage];
return bezierImageView;
}
And you create your button like:
// myButton is your custom button. spinView is an UIImageView, you keep reference on it to apply or to stop animation
self.spinView = [self drawBezierPathInView:mybutton];
[mybutton addSubview:self.spinView];
[self runSpinAnimationOnView:self.spinView duration:3 repeat:HUGE_VALF];

Related

Circular UIButtons Getting Distorted on Rotation Animation Due to Resize

I have this 10 keypad on iPhone that has basically the same screen as the iPhone unlock screen, only mine rotates which resizes the buttons to fit the screen. The problem is that the animation of rotating the device distorts the round shape because they have changed size, but the cornerRadius is the same until it completes the animation. Once the rotation animation has completed, the buttons get the radius set again, thus making them round. I don't know how to have the UIButtons always round, specifically during the rotation animation. Here's basically what I have:
- (void)viewDidLayoutSubviews {
[self setupButtons];
}
- (void)setupButtons {
for (UIButton *button in self.touchPadButtons) {
[self roundifyButton:button withRadius:radius];
}
}
- (void)roundifyButton:(UIButton *)button {
NSInteger height = button.frame.size.height;
radius = height/2;
button.layer.cornerRadius = radius;
button.layer.borderWidth = .6f;
button.layer.borderColor = [UIColor whiteColor].CGColor;
button.clipsToBounds = YES;
}
I've tried using:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
but it seems like in order for setting the radius to work from that method, I'd have to set the size of my buttons programmatically instead of using autolayout. Does anyone have any magical suggestions on handling this? I would sure love to not rotate, like Apple, but unfortunately that decision was not mine.
Wow, this was tougher than I thought it would be. Fortunately, WWDC is going on right now and I was able to get a solution from the Interface Builder lab. His solution was to subclass UIButton and overwrite the drawRect: method. So this is the only method you should have in the CircleButton class. One issue I found is that the lineWidth property doesn't get set before it's initialized by the nib. I overwrote the init method and set a default value, but it doesn't get hit the first time when the nib initializes the buttons. So I had to add the default value in the drawRect: method. I hope this helps people who need circular UIButtons that can resize.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
In case you want to animate the button click the way iPhone lock-screen does, you'll need to add this to the CircleButton class. Otherwise only the titleLabel will be highlighted.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
self.layer.cornerRadius = radius;
self.clipsToBounds = YES;
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// highlight button on click
self.backgroundColor = [UIColor lightGrayColor];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
// remove highlight
self.backgroundColor = [UIColor clearColor];
}

Animate path of shape layer

I'm stumped by what I thought would be a simple problem.
I'd like to draw views connected by lines, animate the position of the views and have the connecting line animate too. I create the views, and create a line between them like this:
- (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint: pointA];
[linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
[linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
[linePath addLineToPoint:pointB];
return linePath;
}
-(void)makeTheLine {
CGPoint pointA = self.viewA.center;
CGPoint pointB = self.viewB.center;
CAShapeLayer *lineShape = [CAShapeLayer layer];
UIBezierPath *linePath=[self pathFrom:pointA to:pointB];
lineShape.path=linePath.CGPath;
lineShape.fillColor = nil;
lineShape.opacity = 1.0;
lineShape.strokeColor = [UIColor blackColor].CGColor;
[self.view.layer addSublayer:lineShape];
self.lineShape = lineShape;
}
It draws just how I want it to. My understanding from the docs is that I am allowed to animate a shape's path by altering it in an animation block, like this:
- (void)moveViewATo:(CGPoint)dest {
UIBezierPath *destPath=[self pathFrom:dest to:self.viewB.center];
[UIView animateWithDuration:1 animations:^{
self.viewA.center = dest;
self.lineShape.path = destPath.CGPath;
}];
}
But no dice. The view position animates as expected, but the line connecting to the other view "jumps" right away to the target path.
This answer implies that what I'm doing should work. And this answer suggests a CABasic animation, which seems worse to me since (a) I'd then need to coordinate with the much cooler block animation done to the view, and (b) when I tried it this way, the line didn't change at all....
// worse way
- (void)moveViewATo:(CGPoint)dest {
UIBezierPath *linePath=[self pathFrom:dest to:self.viewB.center];
[UIView animateWithDuration:1 animations:^{
self.viewA.center = dest;
//self.lineShape.path = linePath.CGPath;
}];
CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:#"path"];
morph.duration = 1;
morph.toValue = (id)linePath.CGPath;
[self.view.layer addAnimation:morph forKey:nil];
}
Thanks in advance.
Thanks all for the help. What I discovered subsequent to asking this is that I was animating the wrong property. It turns out, you can replace the layer's shape in an animation, like this:
CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:#"path"];
morph.duration = 1;
morph.fromValue = (__bridge id)oldPath.path;
morph.toValue = (__bridge id)newPath.CGPath;
[line addAnimation:morph forKey:#"change line path"];
line.path=linePath.CGPath;
I guess this is all you need:
#import "ViewController.h"
#interface ViewController ()
//the view to animate, nothing but a simple empty UIView here.
#property (nonatomic, strong) IBOutlet UIView *targetView;
#property (nonatomic, strong) CAShapeLayer *shapeLayer;
#property NSTimeInterval animationDuration;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//the shape layer appearance
self.shapeLayer = [[CAShapeLayer alloc]init];
self.shapeLayer.strokeColor = [UIColor blackColor].CGColor;
self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
self.shapeLayer.opacity = 1.0;
self.shapeLayer.lineWidth = 2.0;
[self.view.layer insertSublayer:self.shapeLayer below:self.targetView.layer];
//animation config
self.animationDuration = 2;
}
- (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint: pointA];
[linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
[linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
[linePath addLineToPoint:pointB];
return linePath;
}
- (void) moveViewTo: (CGPoint) point {
UIBezierPath *linePath= [self pathFrom:self.targetView.center to:point];
self.shapeLayer.path = linePath.CGPath;
//Use CAKeyframeAnimation to animate the view along the path
//animate the position of targetView.layer instead of the center of targetView
CAKeyframeAnimation *viewMovingAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
viewMovingAnimation.duration = self.animationDuration;
viewMovingAnimation.path = linePath.CGPath;
//set the calculationMode to kCAAnimationPaced to make the movement in a constant speed
viewMovingAnimation.calculationMode =kCAAnimationPaced;
[self.targetView.layer addAnimation:viewMovingAnimation forKey:viewMovingAnimation.keyPath];
//draw the path, animate the keyPath "strokeEnd"
CABasicAnimation *lineDrawingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
lineDrawingAnimation.duration = self.animationDuration;
lineDrawingAnimation.fromValue = [NSNumber numberWithDouble: 0];
lineDrawingAnimation.toValue = [NSNumber numberWithDouble: 1];
[self.shapeLayer addAnimation:lineDrawingAnimation forKey:lineDrawingAnimation.keyPath];
//This part is crucial, update the values, otherwise it will back to its original state
self.shapeLayer.strokeEnd = 1.0;
self.targetView.center = point;
}
//the IBAction for a UITapGestureRecognizer
- (IBAction) viewDidTapped:(id)sender {
//move the view to the tapped location
[self moveViewTo:[sender locationInView: self.view]];
}
#end
Some explanation:
For UIViewAnimation, the property value is changed when the
animation is completed. For CALayerAnimation, the property value is
never change, it is just an animation and when the animation is
completed, the layer will go to its original state (in this case, the
path).
Putting self.lineShape.path = linePath.CGPath doesn't work is
because self.linePath is a CALayer instead of a UIView, you
have to use CALayerAnimation to animate a CALayer
To draw a path, it's better to animate the path drawing with keyPath
strokeEnd instead of path. I'm not sure why path worked in the
original post, but it seems weird to me.
CAKeyframeAnimation (instead of CABasicAnimation or UIViewAnimation) is used to animate the view along the path. (I guess you would prefer this to the linear animation directly from start point to end point). Setting calculationMode to kCAAnimationPaced will give a constant speed to the animation, otherwise the view moving will not sync with the line drawing.

Animate an arc around centre?

I'm porting some animation code that looks a bit like this:
- (void)drawRect:(CGRect)rect
{
self.angle += 0.1;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.height/2, self.frame.size.height/2, //center
self.frame.size.height/2 - 2, //radius
0.0 + self.angle, M_PI_4 + self.angle, //arc start/finish
NO);
CGContextStrokePath(context);
}
The problem is that drawRect is only ever called once, when the view is first drawn, so the position of the arc is never updated.
How can I achieve the effect I want (the arc slowly and continuously moving around the centre point)? Most of the animation examples I can find are to perform a one-time animation (such as a fade-In), but not something that is continuous.
I've also tried something along the lines of:
[arcView animateWithDuration:10.0f
delay:1.0f
options: UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState
animations: ^(void){
_arcView.transform = CGAffineTransformMakeRotation(self.angle++);
}
completion:NULL];
When showing the view, but this doesn't seem to be doing anything either.
A little more about what I'm aiming for: I have a View that I want to be able to set certain states on, e.g. arcView.state = STATE_READY, and for that to change the way it animates. This is being ported from an Android project where it's as simple as adding logic to the draw method on the View, and something reasonably analogous would be preferred.
A couple of observations:
First, drawRect should probably not be incrementing the angle. The purpose of drawRect is to draw a single frame and it should not be changing the state of the view.
If you wanted the UIView subclass to repeated update the angle and redraw itself, you would set up a timer, or better, a CADisplayLink, that will be called periodically, updating the angle and then calling [self setNeedsDisplay] to update the view:
#import "MyView.h"
#interface MyView ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval startTime;
#property (nonatomic) CGFloat revolutionsPerSecond;
#property (nonatomic) CGFloat angle;
#end
#implementation MyView
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.angle = 0.0;
self.revolutionsPerSecond = 0.25; // i.e. go around once per every 4 seconds
[self startDisplayLink];
}
return self;
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval elapsed = CACurrentMediaTime() - self.startTime;
double revolutions;
double percent = modf(elapsed * self.revolutionsPerSecond, &revolutions);
self.angle = M_PI * 2.0 * percent;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.width/2, self.frame.size.height/2, //center
MIN(self.frame.size.width, self.frame.size.height) / 2.0 - 2.0, //radius
0.0 + self.angle, M_PI_4 + self.angle, //arc start/finish
NO);
CGContextStrokePath(context);
}
#end
An easier approach is to update the transform property of the view, to rotate it. In iOS 7 and later, you add a view with the arc and then rotate it with something like:
[UIView animateKeyframesWithDuration:4.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat | UIViewAnimationOptionCurveLinear animations:^{
[UIView addKeyframeWithRelativeStartTime:0.00 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI_2);
}];
[UIView addKeyframeWithRelativeStartTime:0.25 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI);
}];
[UIView addKeyframeWithRelativeStartTime:0.50 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI_2 * 3.0);
}];
[UIView addKeyframeWithRelativeStartTime:0.75 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI * 2.0);
}];
} completion:nil];
Note, I've broken the animation up into four steps, because if you rotate from 0 to M_PI * 2.0, it won't animate because it knows the ending point is the same as the starting point. So by breaking it up into four steps like that it does each of the four animations in succession. If doing in in earlier versions of iOS, you do something similar with animateWithDuration, but to have it repeat, you need the completion block to invoke another rotation. Something like: https://stackoverflow.com/a/19408039/1271826
Finally, if you want to animate, for example, just the end of the arc (i.e. to animate the drawing of the arc), you can use CABasicAnimation with a CAShapeLayer:
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0) radius:MIN(self.view.bounds.size.width, self.view.bounds.size.height) / 2.0 - 2.0 startAngle:0 endAngle:M_PI_4 / 2.0 clockwise:YES];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.frame = self.view.bounds;
layer.path = path.CGPath;
layer.lineWidth = 2.0;
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:layer];
CABasicAnimation *animateStrokeEnd = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animateStrokeEnd.duration = 10.0;
animateStrokeEnd.fromValue = #0.0;
animateStrokeEnd.toValue = #1.0;
animateStrokeEnd.repeatCount = HUGE_VALF;
[layer addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
I know that's probably not what you're looking for, but I mention it just so you can add it to your animation "tool belt".
Most of the animation examples I can find are to perform a one-time
animation (such as a fade-In), but not something that is continuous.
If you use a method like UIView's +animateWithDuration:delay:options:animations:completion: to do your animation, you can specify UIViewAnimationOptionRepeat in the options parameter to make the animation repeat indefinitely.
Fundamentally, using -drawRect: for most animations is the wrong way to go. As you discovered, -drawRect: is only called when the graphics system really needs to redraw the view, and that's a relatively expensive process. As much as possible, you should use Core Animation to do your animations, especially for stuff like this where the view itself doesn't need to be redrawn, but it's pixels need to be transformed somehow.
Note that +animateWithDuration:delay:options:animations:completion: is a class method, so you should send it to UIView itself (or some subclass of UIView), not to an instance of UIView. There's an example here. Also, that particular method might or might not be the best choice for what you're doing -- I just called it out because it's an easy example of using animation with views and it lets you specify the repeating option.
I'm not sure what's wrong with your animation (other than maybe the wrong receiver as described above), but I'd avoid using the ++ operator to modify the angle. The angle is specified in radians, so incrementing by 1 probably isn't what you want. Instead, try adding π/2 to the current rotation, like this:
_arcView.transform = CAAffineTransformRotate(_arcView.transform, M_PI_2);
So this is what I've ended up with. It took a long time to work out the I needed "translation.rotation" instead of just "rotation"...
#implementation arcView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
self.state = 0;
if (self) {
self.layer.contents = (__bridge id)([[self generateArc:[UIColor redColor].CGColor]CGImage]);
}
return self;
}
-(UIImage*)generateArc:(CGColorRef)color{
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, color);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.height/2, self.frame.size.height/2,
self.frame.size.height/2 - 2,0.0,M_PI_4,NO);
CGContextStrokePath(context);
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
-(void)spin{
[self.layer removeAnimationForKey:#"rotation"];
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
NSNumber *current =[[self.layer presentationLayer] valueForKeyPath:#"transform.rotation"];
rotate.fromValue = current;
rotate.toValue = [NSNumber numberWithFloat:current.floatValue + M_PI * (self.state*2 - 1)];
rotate.duration = 3.0;
rotate.repeatCount = INT_MAX;
rotate.fillMode = kCAFillModeForwards;
rotate.removedOnCompletion = NO;
[self.layer addAnimation:rotate forKey:#"rotation"];
}

Animate a CAShapeLayer to draw a progress circle

I am trying to draw a circle that will be used to indicate progress. The progress updates might come in quickly, and I want to animate the changes to the circle.
I have tried doing this with the below methods, but nothing seems to work. Is this possible?
- (id)initWithFrame:(CGRect)frame strokeWidth:(CGFloat)strokeWidth insets:(UIEdgeInsets)insets
{
if (self = [super initWithFrame:frame])
{
self.strokeWidth = strokeWidth;
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = CGRectGetMidX(self.bounds) - insets.top - insets.bottom;
self.circlePath = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:M_PI
endAngle:-M_PI
clockwise:YES];
[self addLayer];
}
return self;
}
- (void)setProgress:(double)progress {
_progress = progress;
[self updateAnimations];
}
- (void)addLayer {
CAShapeLayer *progressLayer = [CAShapeLayer layer];
progressLayer.path = self.circlePath.CGPath;
progressLayer.strokeColor = [[UIColor whiteColor] CGColor];
progressLayer.fillColor = [[UIColor clearColor] CGColor];
progressLayer.lineWidth = self.strokeWidth;
progressLayer.strokeStart = 10.0f;
progressLayer.strokeEnd = 40.0f;
[self.layer addSublayer:progressLayer];
self.currentProgressLayer = progressLayer;
}
- (void)updateAnimations{
self.currentProgressLayer.strokeEnd = self.progress;
[self.currentProgressLayer didChangeValueForKey:#"endValue"];
}
Currently this code doesn't even draw the circle, but removing the progressLayer's strokeStart and strokeEnd will draw the full circle.
How can I get it to begin at a certain point and start drawing the circle based on me setting my progress property?
I figured it out. The strokeStart and strokeEnd values are from 0.0 to 1.0, so the values I was giving it were way too high.
So, I just updated my setting to:
- (void)setProgress:(double)progress {
_progress = progress * 0.00460;
[self updateAnimations];
}
My answer with example here. In general you should add the animation to CAShapeLayer.

CAShapeLayer animation (arc from 0 to final size)

I have a CAShapeLayer in which an arc is added using UIBezierPath. I saw a couple of posts (one actually) here on stackoverflow but none seemed to give me the answer. As said in the title I would like to animate an arc (yes it will be for a pie chart).
How can one accomplish an animation from an "empty" arc to the fully extended one? Kinda like a curved progress bar. Is it really impossible to do it via CoreAnimation?
Here's how I "do" the arc. Oh and ignore the comments and calculation since I'm used to counter clockwise unit circle and apple goes clockwise. The arcs appear just fine, I only need animation:
//Apple's unit circle goes in clockwise rotation while im used to counter clockwise that's why my angles are mirrored along x-axis
workAngle = 6.283185307 - DEGREES_TO_RADIANS(360 * item.value / total);
//My unit circle, that's why negative currentAngle
[path moveToPoint:CGPointMake(center.x + cosf(outerPaddingAngle - currentAngle) * (bounds.size.width / 2), center.y - (sinf(outerPaddingAngle - currentAngle) * (bounds.size.height / 2)))];
//Apple's unit circle, clockwise rotation, we add the angles
[path addArcWithCenter:center radius:120 startAngle:currentAngle endAngle:currentAngle + workAngle clockwise:NO];
//My unit circle, counter clockwise rotation, we subtract the angles
[path addLineToPoint:CGPointMake(center.x + cosf(-currentAngle - workAngle) * ((bounds.size.width / 2) - pieWidth), center.y - sinf(-currentAngle - workAngle) * ((bounds.size.height / 2) - pieWidth))];
//Apple's unit circle, clockwise rotation, addition of angles
[path addArcWithCenter:center radius:120 - pieWidth startAngle:currentAngle + workAngle endAngle:currentAngle - innerPaddingAngle clockwise:YES];
//No need for my custom calculations since I can simply close the path
[path closePath];
shape = [CAShapeLayer layer];
shape.frame = self.bounds;
shape.path = path.CGPath;
shape.fillColor = kRGBAColor(255, 255, 255, 0.2f + 0.1f * (i + 1)).CGColor; //kRGBAColor is a #defined
[self.layer addSublayer:shape];
//THIS IS THE PART IM INTERESTED IN
if (_shouldAnimate)
{
CABasicAnimation *pieAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pieAnimation.duration = 1.0;
pieAnimation.removedOnCompletion = NO;
pieAnimation.fillMode = kCAFillModeForwards;
//from and to are just dummies so I dont get errors
pieAnimation.fromValue = [NSNumber numberWithInt:0]; //from what
pieAnimation.toValue = [NSNumber numberWithFloat:1.0f]; //to what
[shape addAnimation:pieAnimation forKey:#"animatePath"];
}
Here is an implementation of a CALayer based on CAShapeLayer:
ProgressLayer.h/m
#import <QuartzCore/QuartzCore.h>
#interface ProgressLayer : CAShapeLayer
-(void) computePath:(CGRect)r;
-(void) showProgress;
#end
#import "ProgressLayer.h"
#implementation ProgressLayer
-(id)init {
self=[super init];
if (self) {
self.path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 50, 50), 0);
self.strokeColor = [UIColor greenColor].CGColor;
self.lineWidth=40;
self.lineCap = kCALineCapRound;
self.strokeEnd=0.001;
}
return self;
}
-(void) computePath:(CGRect)r {
self.path = CGPathCreateWithEllipseInRect(r, 0);
}
-(void)showProgress {
float advance=0.1;
if (self.strokeEnd>1) self.strokeEnd=0;
CABasicAnimation * swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration=0.25;
swipe.fromValue=[NSNumber numberWithDouble:self.strokeEnd];
swipe.toValue= [NSNumber numberWithDouble:self.strokeEnd + advance];
swipe.fillMode = kCAFillModeForwards;
swipe.timingFunction= [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
swipe.removedOnCompletion=NO;
self.strokeEnd = self.strokeEnd + advance;
[self addAnimation:swipe forKey:#"strokeEnd animation"];
}
#end
I used this layer as a backing layer of an UIView:
#import "ProgressView.h"
#import "ProgressLayer.h"
#implementation ProgressView
-(id)initWithCoder:(NSCoder *)aDecoder {
self=[super initWithCoder:aDecoder];
if (self) {
[(ProgressLayer *)self.layer computePath:self.bounds];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
ProgressLayer * l = self.layer;
[l showProgress];
}
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setStroke];
UIRectFrame(self.bounds);
}
+(Class)layerClass {
return [ProgressLayer class];
}
#end
Before you create your animation you want to set your shape's strokeEnd to 0.
Then, use the key #"strokeEnd" instead of #"path"... Keys for CAAnimations are generally the name of the property you want to animate.
Then you can do the following:
[CATransaction begin]
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1.0f;
animation.removedOnCompletion = NO;
animation.fromValue = #0; //shorthand for creating an NSNumber
animation.toValue = #1; //shorthand for creating an NSNumber
[self addAnimation:animation forKey:#"animateStrokeEnd"];
[CATransaction commit];
You have to use the proper keys. "path" and "animatePath" don't mean anything to it - you have to use "strokeEnd" for your key/keypath. If you need to fiddle with your fill color, you use fillColor. The full list is here:
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAShapeLayer_class/Reference/Reference.html

Resources