Animate path of shape layer - ios

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.

Related

Circular loading animation (like Weather Channel App)

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];

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

Animate rotation and location of view at same time

What is the best way to rotate a view and move its location at the same time?
I want this view to rotate 1 full revolution while sliding to the left, then rotate the other direction while sliding back to its original position. I got the first part of this working (rotate while sliding to left), but when I try to rotate while sliding back to the right, the view jumps back to its original spot without animating.
What am I doing wrong?
// step 1 (this works)
[UIView animateWithDuration:0.3f animations:^{
self.number.transform = CGAffineTransformMakeRotation(DegreesToRadians(180));
self.number.transform = CGAffineTransformMakeTranslation(-160, 0);
self.number.center = CGPointMake(self.number.center.x - 160, self.number.center.y);
} completion:nil];
// step 2 (this doesn't work)
[UIView animateWithDuration:0.2f animations:^{
self.number.transform = CGAffineTransformMakeRotation(DegreesToRadians(180));
self.number.transform = CGAffineTransformMakeTranslation(160, 0);
self.number.center = CGPointMake(self.number.center.x + 160, self.number.center.y);
} completion:nil];
I got it to work using the following code. My view had a constraint to the left side of the superview, to which I added the IBOutlet, leftCon. It's initial value was 160, so the translation moves it all the way to the left edge of the screen.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#define animationTime 1.0
#interface ViewController ()
#property (nonatomic) CGFloat leftConstant;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.leftConstant = self.leftCon.constant;
}
- (IBAction)rotate:(UIButton *)sender {
if (self.leftCon.constant == self.leftConstant) {
[self.view removeConstraint:self.leftCon];
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(-M_PI * 2.0);
rotationAnimation.duration = animationTime;
CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
translationAnimation.fromValue = [NSValue valueWithCGPoint:self.number.center];
translationAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.number.frame.size.width/2.0, self.number.center.y)];
translationAnimation.duration = animationTime;
[self.number.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[self.number.layer addAnimation:translationAnimation forKey:#"translationAnimation"];
self.leftCon.constant = 0;
[self.view addConstraint:self.leftCon];
}else{
[self.view removeConstraint:self.leftCon];
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(M_PI * 2.0);
rotationAnimation.duration = animationTime;
CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
translationAnimation.fromValue = [NSValue valueWithCGPoint:self.number.center];
translationAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.leftConstant+ self.number.frame.size.width/2.0, self.number.center.y)];
translationAnimation.duration = animationTime;
[self.number.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[self.number.layer addAnimation:translationAnimation forKey:#"translationAnimation"];
self.leftCon.constant = self.leftConstant;
[self.view addConstraint:self.leftCon];
}
}
You probably want to place your second animation as the completion block in the first animation.

iOS: CAShapeLayer to draw non-rectagular image and animating its shape

I have been reading the documentation on CAShapeLayer but I still don't quite get it.
From my understanding, Layer is always flat and its size is always a rectangle.
CAShapeLayer on the other hand, allows you to define a layer that is not just rectangle-like. It can be a circle shape, triangle etc as long as you use it with UIBezierPaths.
Is my understanding off here?
What I had in mind is, for example, a tennis ball that bounces off the edges on the screen (easy enough), but I would like to show a little animation not using image animations - I would like it to show a little "squeezed" like animation as it hits the edge of the screen and then bounces off. I am not using a tennis ball image. Just a yellow color filled circle.
Am I correct here with CAShapeLayer to accomplish this? If so, can you please provide a litle example? Thanks.
While you do use a path to define the shape of the layer, it is still created with a rectangle used to define the frame/bounds. Here is an example to get you started:
TennisBall.h:
#import <UIKit/UIKit.h>
#interface TennisBall : UIView
- (void)bounce;
#end
TennisBall.m:
#import <QuartzCore/QuartzCore.h>
#import "TennisBall.h"
#implementation TennisBall
+ (Class)layerClass
{
return [CAShapeLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setupLayer];
}
return self;
}
/* Create the tennis ball */
- (void)setupLayer
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.strokeColor = [[UIColor blackColor] CGColor];
layer.fillColor = [[UIColor yellowColor] CGColor];
layer.lineWidth = 1.5;
layer.path = [self defaultPath];
}
/* Animate the tennis ball "bouncing" off of the side of the screen */
- (void)bounce
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 0.2;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)[self defaultPath];
animation.toValue = (__bridge id)[self compressedPath];
animation.autoreverses = YES;
[self.layer addAnimation:animation forKey:#"animatePath"];
}
/* A path representing the tennis ball in the default state */
- (CGPathRef)defaultPath
{
return [[UIBezierPath bezierPathWithOvalInRect:self.frame] CGPath];
}
/* A path representing the tennis ball is the compressed state (during the bounce) */
- (CGPathRef)compressedPath
{
CGRect newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width * 0.85, self.frame.size.height);
return [[UIBezierPath bezierPathWithOvalInRect:newFrame] CGPath];
}
#end
Now, when you want to use this:
TennisBall *ball = [[TennisBall alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
[self.view addSubview:ball];
[ball bounce];
Note that this will need to be extended so that you can "bounce" from different angles, etc. but it should get you pointed in the right direction!

How to make my UIBezierPath animated with CAShapeLayer?

I'm trying to animate a UIBezierPath and I've installed a CAShapeLayer to try to do it. Unfortunately the animation isn't working and I'm not sure any of the layers are having any affect (as the code is doing the same thing it was doing before I had the layers).
Here is the actual code - would love any help. Draw2D is an implementation of UIView that is embedded in a UIViewController. All the drawing is happening inside the Draw2D class. The call to [_helper createDrawing... ] simply populates the _uipath variable with points.
Draw2D.h defines the following properties:
#define defaultPointCount ((int) 25)
#property Draw2DHelper *helper;
#property drawingTypes drawingType;
#property int graphPoints;
#property UIBezierPath *uipath;
#property CALayer *animationLayer;
#property CAShapeLayer *pathLayer;
- (void)refreshRect:(CGRect)rect;
below is the actual implementation :
//
// Draw2D.m
// Draw2D
//
// Created by Marina on 2/19/13.
// Copyright (c) 2013 Marina. All rights reserved.
//
#import "Draw2D.h"
#import"Draw2DHelper.h"
#include <stdlib.h>
#import "Foundation/Foundation.h"
#import <QuartzCore/QuartzCore.h>
int MAX_WIDTH;
int MAX_HEIGHT;
#implementation Draw2D
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
if (self.pathLayer != nil) {
[self.pathLayer removeFromSuperlayer];
self.pathLayer = nil;
}
self.animationLayer = [CALayer layer];
self.animationLayer.frame = self.bounds;
[self.layer addSublayer:self.animationLayer];
CAShapeLayer *l_pathLayer = [CAShapeLayer layer];
l_pathLayer.frame = self.frame;
l_pathLayer.bounds = self.bounds;
l_pathLayer.geometryFlipped = YES;
l_pathLayer.path = _uipath.CGPath;
l_pathLayer.strokeColor = [[UIColor grayColor] CGColor];
l_pathLayer.fillColor = nil;
l_pathLayer.lineWidth = 1.5f;
l_pathLayer.lineJoin = kCALineJoinBevel;
[self.animationLayer addSublayer:l_pathLayer];
self.pathLayer = l_pathLayer;
[self.layer addSublayer:l_pathLayer];
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect :(int) points :(drawingTypes) type //:(Boolean) initial
{
//CGRect bounds = [[UIScreen mainScreen] bounds];
CGRect appframe= [[UIScreen mainScreen] applicationFrame];
CGContextRef context = UIGraphicsGetCurrentContext();
_helper = [[Draw2DHelper alloc ] initWithBounds :appframe.size.width :appframe.size.height :type];
CGPoint startPoint = [_helper generatePoint] ;
[_uipath moveToPoint:startPoint];
[_uipath setLineWidth: 1.5];
CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
CGPoint center = CGPointMake(self.center.y, self.center.x) ;
[_helper createDrawing :type :_uipath :( (points>0) ? points : defaultPointCount) :center];
self.pathLayer.path = (__bridge CGPathRef)(_uipath);
[_uipath stroke];
[self startAnimation];
}
- (void) startAnimation {
[self.pathLayer removeAllAnimations];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
- (void)drawRect:(CGRect)rect {
if (_uipath == NULL)
_uipath = [[UIBezierPath alloc] init];
else
[_uipath removeAllPoints];
[self drawRect:rect :self.graphPoints :self.drawingType ];
}
- (void)refreshRect:(CGRect)rect {
[self setNeedsDisplay];
}
#end
I know there's probably an obvious reason for why the path isn't animating as it's being drawing (as opposed to being shown immediately which is what happens now) but I've been staring at the thing for so long that I just don't see it.
Also, if anyone can recommend a basic primer on CAShapeLayers and animation in general I would appreciate it. Haven't come up across any that are good enough.
thanks in advance.
It looks like you're trying to animate within drawRect (indirectly, at least). That doesn't quite make sense. You don't animate within drawRect. The drawRect is used for drawing a single frame. Some animation is done with timers or CADisplayLink that repeatedly calls setNeedsDisplay (which will cause iOS to call your drawRect) during which you might draw the single frame that shows the progress of the animation at that point. But you simply don't have drawRect initiating any animation on its own.
But, since you're using Core Animation's CAShapeLayer and CABasicAnimation, you don't need a custom drawRect at all. Quartz's Core Animation just takes care of everything for you. For example, here is my code for animating the drawing of a UIBezierPath:
#import <QuartzCore/QuartzCore.h>
#interface View ()
#property (nonatomic, weak) CAShapeLayer *pathLayer;
#end
#implementation View
/*
// I'm not doing anything here, so I can comment this out
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
*/
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
// It doesn't matter what my path is. I could make it anything I wanted.
- (UIBezierPath *)samplePath
{
UIBezierPath *path = [UIBezierPath bezierPath];
// build the path here
return path;
}
- (void)startAnimation
{
if (self.pathLayer == nil)
{
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self samplePath] CGPath];
shapeLayer.strokeColor = [[UIColor grayColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 1.5f;
shapeLayer.lineJoin = kCALineJoinBevel;
[self.layer addSublayer:shapeLayer];
self.pathLayer = shapeLayer;
}
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = #(0.0f);
pathAnimation.toValue = #(1.0f);
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
#end
Then, when I want to start drawing the animation, I just call my startAnimation method. I probably don't even need a UIView subclass at all for something as simple as this, since I'm not actually changing any UIView behavior. There are definitely times that you subclass UIView with a custom drawRect implementation, but it's not needed here.
You asked for some references:
I would probably start with a review of Apple's Core Animation Programming Guide, if you haven't seen that.
For me, it all fell into place when I went through Mike Nachbaur's Core Animation Tutorial Part 4, actually reproducing his demo from scratch. Clearly, you can check out parts 1 through 3, too.

Resources