I try to change a circle into a square and vice versa and I am almost there. However it doesn't animates as expected. I would like all corners of a square to be animated/morphed at the same time but what I get is the following:
I use CAShapeLayer and CABasicAnimation in order to animate shape property.
Here is how I create the circle path:
- (UIBezierPath *)circlePathWithCenter:(CGPoint)center radius:(CGFloat)radius
{
UIBezierPath *circlePath = [UIBezierPath bezierPath];
[circlePath addArcWithCenter:center radius:radius startAngle:0 endAngle:M_PI/2 clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:M_PI/2 endAngle:M_PI clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:3*M_PI/2 clockwise:YES];
[circlePath addArcWithCenter:center radius:radius startAngle:3*M_PI/2 endAngle:M_PI clockwise:YES];
[circlePath closePath];
return circlePath;
}
Here is the square path:
- (UIBezierPath *)squarePathWithCenter:(CGPoint)center size:(CGFloat)size
{
CGFloat startX = center.x-size/2;
CGFloat startY = center.y-size/2;
UIBezierPath *squarePath = [UIBezierPath bezierPath];
[squarePath moveToPoint:CGPointMake(startX, startY)];
[squarePath addLineToPoint:CGPointMake(startX+size, startY)];
[squarePath addLineToPoint:CGPointMake(startX+size, startY+size)];
[squarePath addLineToPoint:CGPointMake(startX, startY+size)];
[squarePath closePath];
return squarePath;
}
I apply the circle path to one of my View's layers, set the fill etc. It draws perfectly.
Then in my gestureRecognizer selector I create and run the following animation:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge id)(self.stateLayer.path);
animation.toValue = (__bridge id)(self.stopPath.CGPath);
self.stateLayer.path = self.stopPath.CGPath;
[self.stateLayer addAnimation:animation forKey:#"animatePath"];
As you can notice in the circlePathWithCenter:radius: and squarePathWithCenter:size: I follow the suggestion from here (to have the same number of segments and control points):
Smooth shape shift animation
The animation looks better then in the post from above but it is still not the one I want to achieve :(
I know that I can do it with simple CALayer and then set appropriate level of cornerRadius to make a circle out of square/rectangle and after that animate cornerRadius property to change it from circle to square but I'm still extremaly curious if it is even possible to do it with CAShapeLayer and path animation.
Thanks in advance for help!
After playing with it for a little while in the playground I noticed that each arc adds two segments to the bezier path, not just one. Moreover, calling closePath adds two more. So to keep the number of segments consistent, I ended up with adding fake segments to my square path. The code is in Swift, but I think it doesn't matter.
func circlePathWithCenter(center: CGPoint, radius: CGFloat) -> UIBezierPath {
let circlePath = UIBezierPath()
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI), endAngle: -CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI/2), endAngle: 0, clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(M_PI), clockwise: true)
circlePath.closePath()
return circlePath
}
func squarePathWithCenter(center: CGPoint, side: CGFloat) -> UIBezierPath {
let squarePath = UIBezierPath()
let startX = center.x - side / 2
let startY = center.y - side / 2
squarePath.moveToPoint(CGPoint(x: startX, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.closePath()
return squarePath
}
I know this is an old question but this might help someone.
I think its better to animate corner radius to get this effect.
let v1 = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
v1.backgroundColor = UIColor.green
view.addSubview(v1)
animation:
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.fromValue = v1.layer.cornerRadius
animation.toValue = v1.bounds.width/2
animation.duration = 3
v1.layer.add(animation, forKey: "cornerRadius")
Based on #Danchoys answer, here it is for Objective-C.
info is a button of 60px and infoVC is the next ViewController being pushed:
UIBezierPath * maskPath = [UIBezierPath new];
[maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(180) endAngle:DEGREES_TO_RADIANS(270) clockwise:true];
[maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(270) endAngle:DEGREES_TO_RADIANS(0) clockwise:true];
[maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(90) clockwise:true];
[maskPath addArcWithCenter:info.center radius:15.0f startAngle:DEGREES_TO_RADIANS(90) endAngle:DEGREES_TO_RADIANS(180) clockwise:true];
[maskPath closePath];
UIBezierPath * endPath = [UIBezierPath new];
[endPath moveToPoint:CGPointMake(0,0)];
[endPath addLineToPoint:endPath.currentPoint];
[endPath addLineToPoint:CGPointMake(w, 0)];
[endPath addLineToPoint:endPath.currentPoint];
[endPath addLineToPoint:CGPointMake(w, h)];
[endPath addLineToPoint:endPath.currentPoint];
[endPath addLineToPoint:CGPointMake(0, h)];
[endPath addLineToPoint:endPath.currentPoint];
[endPath closePath];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = info.bounds;
maskLayer.path = maskPath.CGPath;
_infoVC.view.layer.mask = maskLayer;
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.fromValue = (id)maskPath.CGPath;
animation.toValue = (id)endPath.CGPath;
animation.duration = 0.3f;
[maskLayer addAnimation:animation forKey:nil];
[maskLayer setPath:endPath.CGPath];
I can highly recommend the library CanvasPod, it also has a predefined "morph" animation and is easy to integrate. You can also check out examples on the website canvaspod.io.
This is handy for people making a recording button and want a native iOS like animation.
Instantiate this class within the UIViewController you would like to have the button. Add a UITapGestureRecogniser and call animateRecordingButton when tapped.
import UIKit
class RecordButtonView: UIView {
enum RecordingState {
case ready
case recording
}
var currentState: RecordingState = .ready
override init(frame: CGRect) {
super.init(frame: frame)
self.isUserInteractionEnabled = true
self.backgroundColor = .white
// My button is 75 points in height and width so this
// Will make a circle
self.layer.cornerRadius = 35
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func animateRecordingButton() {
var newRadius: CGFloat
var scale: CGFloat
switch currentState {
case .ready:
// The square radius
newRadius = 10
// Lets scale it down to 70%
scale = 0.7
currentState = .recording
case .recording:
// Animate back to cirlce
newRadius = 35
currentState = .ready
// Animate back to full scale
scale = 1
}
UIView.animate(withDuration: 1) {
self.layer.cornerRadius = newRadius
self.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
}
Related
I am trying to draw a piece of pie in SCNShape using the following code:
UIBezierPath *piePiece = [UIBezierPath bezierPath];
[piePiece addArcWithCenter: CGPointZero radius: 0.150 startAngle: 0.0 endAngle: M_PI/6 clockwise: YES];
[piePiece closePath];
SCNShape *pieShape = [SCNShape shapeWithPath: piePiece extrusionDepth: 0];
pieShape.firstMaterial.diffuse.contents = [UIColor blueColor];
pieShape.firstMaterial.doubleSided = YES;
SCNNode *pieNode = [SCNNode nodeWithGeometry: pieShape];
But I get the following shape:
Sample
I don't see the arc. What am I doing wrong?
Thanks
You must change the flatness of the UIBezierPath to a lower value. In Swift:
pieShape.flatness = 0.0003
I have a view in which I want to draw a circle with UIBezierPath bezierPathWithArcCenter. My view's initWithFrame is as follows:
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self == nil) {
return nil;
}
self.circleLayer = [CAShapeLayer layer];
self.circleLayer.fillColor = UIColor.clearColor.CGColor;
self.circleLayer.strokeColor = UIColor.blackColor.CGColor;
self.circleLayer.lineWidth = 4;
self.circleLayer.bounds = self.bounds;
self.startAngle = 0.0;
self.endAngle = M_PI * 2;
CGPoint center = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 0.5);
CGFloat radius = self.frame.size.height * 0.45;
self.circleLayer.path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:self.startAngle endAngle:self.endAngle clockwise:YES].CGPath;
[self.layer addSublayer:self.circleLayer];
self.backgroundColor = UIColor.greenColor;
return self;
}
I expected that would produce a black circle centered in my view. However, the actual output looks like this:
What am I doing wrong?
EDIT
The weirdest part is that it's drawn correctly after a fresh install of the app - any consecutive launches result in it being drawn like in the image attached. Why?
Do not use view's center!
In my case, using ,
let centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
for center point solved my problem.
The reason for this seems to be a bug in iOS. The CAShapeLayer has its bounds set properly
self.circleLayer.bounds = self.bounds;
Upon inspecting it, the origin and size of the layer is fine, but only after first launch of the app after installing. Any subsequent launch of the app will result in its size being (0, 0). Removing the app and installing it again will result in the circle being drawn properly once.
I fixed it by setting the layer's frame again further down the road.
I ran into somewhat same problem, but for me the path was not drawing in center of my view even on the first launch.
This was my code at the time of problem:
let circularPath = UIBezierPath(arcCenter: CGPoint(x: containerView.frame.size.width/2, y: containerView.frame.size.height/2), radius: 50, startAngle: (-CGFloat.pi / 2), endAngle: (CGFloat.pi * 2), clockwise: true)
the 'containerView' is the view containing the circle drawn by bazierpath.
but then I did the following on a hunch and it worked.
let centerPoint = CGPoint(x: containerView.frame.size.width/2, y: containerView.frame.size.height/2)
let circularPath = UIBezierPath(arcCenter: centerPoint, radius: 50, startAngle: (-CGFloat.pi / 2), endAngle: (CGFloat.pi * 2), clockwise: true)
Basically I had to create the center point before assigning it to the bazierpath. Hope this helps someone.
I am setting a mask with all rounded corners path to a view like this:
CGFloat cornerRadius = CGRectGetHeight(self.myView.bounds) / 2;
CGSize cornerRadiusSize = CGSizeMake(cornerRadius, cornerRadius);
CGRect bounds = self.myView.bounds;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:cornerRadiusSize];
maskLayer.path = maskPath.CGPath;
self.myView.layer.mask = maskLayer;
that looks like this:
I am trying to animate it to a path with top left and top right rounded corners like this:
UIBezierPath *newMaskPath = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:cornerRadiusSize];
CABasicAnimation *maskPathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
maskPathAnimation.duration = 0.3;
maskPathAnimation.fromValue = (id)maskLayer.path;
maskPathAnimation.toValue = (id)newMaskPath.CGPath;
[maskLayer addAnimation:maskPathAnimation forKey:nil];
maskLayer.path = newMaskPath.CGPath;
that looks like this:
But the animation is mangled, it looks like it animates some corners to other corners:
I solved it by creating the mask paths by hand using this method:
- (UIBezierPath *)getMaskPathForView:(UIView *)view withCorners:(UIRectCorner)corners {
CGSize size = view.bounds.size;
CGFloat cornerRadius = size.height / 2;
UIBezierPath *maskPath = [UIBezierPath bezierPath];
// top left
if (corners & UIRectCornerTopLeft) {
[maskPath moveToPoint:CGPointMake(0, cornerRadius)];
[maskPath addQuadCurveToPoint:CGPointMake(cornerRadius, 0) controlPoint:CGPointZero];
} else {
CGPoint topLeftCornerPoint = CGPointZero;
[maskPath moveToPoint:topLeftCornerPoint];
[maskPath addQuadCurveToPoint:topLeftCornerPoint controlPoint:topLeftCornerPoint];
}
// top right
if (corners & UIRectCornerTopRight) {
[maskPath addLineToPoint:CGPointMake(size.width - cornerRadius, 0)];
[maskPath addQuadCurveToPoint:CGPointMake(size.width, cornerRadius) controlPoint:CGPointMake(size.width, 0)];
} else {
CGPoint topRightCornerPoint = CGPointMake(size.width, 0);
[maskPath addLineToPoint:topRightCornerPoint];
[maskPath addQuadCurveToPoint:topRightCornerPoint controlPoint:topRightCornerPoint];
}
// bottom right
if (corners & UIRectCornerBottomRight) {
[maskPath addLineToPoint:CGPointMake(size.width, size.height - cornerRadius)];
[maskPath addQuadCurveToPoint:CGPointMake(size.width - cornerRadius, size.height) controlPoint:CGPointMake(size.width, size.height)];
} else {
CGPoint bottomRightCornerPoint = CGPointMake(size.width, size.height);
[maskPath addLineToPoint:bottomRightCornerPoint];
[maskPath addQuadCurveToPoint:bottomRightCornerPoint controlPoint:bottomRightCornerPoint];
}
// bottom left
if (corners & UIRectCornerBottomLeft) {
[maskPath addLineToPoint:CGPointMake(cornerRadius, size.height)];
[maskPath addQuadCurveToPoint:CGPointMake(0, size.height - cornerRadius) controlPoint:CGPointMake(0, size.height)];
} else {
CGPoint bottomLeftCornerPoint = CGPointMake(0, size.height);
[maskPath addLineToPoint:bottomLeftCornerPoint];
[maskPath addQuadCurveToPoint:bottomLeftCornerPoint controlPoint:bottomLeftCornerPoint];
}
[maskPath closePath];
return maskPath;
}
like this:
CAShapeLayer *maskLayer = [CAShapeLayer layer];
UIBezierPath *maskPath = [self getMaskPathForView:self.myView withCorners:UIRectCornerAllCorners];
maskLayer.path = maskPath.CGPath;
self.myView.layer.mask = maskLayer;
and:
UIBezierPath *newMaskPath = [self getMaskPathForView:self.myView withCorners:UIRectCornerTopLeft | UIRectCornerTopRight];
CABasicAnimation *maskPathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
maskPathAnimation.duration = 0.3;
maskPathAnimation.fromValue = (id)maskLayer.path;
maskPathAnimation.toValue = (id)newMaskPath.CGPath;
[maskLayer addAnimation:maskPathAnimation forKey:nil];
maskLayer.path = newMaskPath.CGPath;
I am trying to create a resizable version of the following image (40x28, the small triangle at the bottom should be centered horizontally):
UIEdgeInsets imageInsets = UIEdgeInsetsMake(4.0f, 4.0f, 8.0f, 4.0f);
UIImage *pinImage = [[UIImage imageNamed:#"map_pin"] resizableImageWithCapInsets:imageInsets];
As per the documentation, this simple approach won't work since my triangle falls into resizable area (fixed height but scalable width):
This will cause the same problem as described in this very similar SO post - duplicated (tiled) triangles. But unfortunately their solution doesn't work for my particular case since excluding the triangle from scalable area causes its misplacement and it won't be centered horizontally due to obvious reasons.
So I wonder... is it possible to achieve my goal using the [UIImage resizableImageWithCapInsets:] method at all? Or may be I should try something else like drawing everything in the code instead?
Basically, I would like to implement something like Info Windows from Google Maps SDK:
You can achieve that by using UIBezierPath.The following code in swift but you should be able to achieve the same thing in Objective-C
Instead of starting the code with a straight line :
path.moveToPoint(CGPoint(x: 300, y: 0))
I instead start with an arc (upper right):
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
and by doing this, I have four rounded corners and I just need to add a straight line at the end of the code right before:
path.closePath()
Here is the code and a screenshot:
let path = UIBezierPath()
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 50), radius:10, startAngle: CGFloat(2 * M_PI / 3), endAngle:CGFloat(M_PI) , clockwise: true)// 2rd rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 10), radius:10, startAngle: CGFloat(M_PI), endAngle:CGFloat(3 * M_PI / 2), clockwise: true)// 3rd rounded corner
// little triangle
path.addLineToPoint(CGPoint(x:240 , y:0))
path.addLineToPoint(CGPoint(x: 245, y: -10))
path.addLineToPoint(CGPoint(x:250, y: 0))
path.addArcWithCenter(CGPoint(x: 290, y: 10), radius: 10, startAngle: CGFloat(3 * M_PI / 2), endAngle: CGFloat(2 * M_PI ), clockwise: true)
path.addLineToPoint(CGPoint(x:300 , y:50))
path.closePath()
I decided to go with UIBezierPath as Assam Al-Zookery suggested. Here is the complete code in Objective-C (should be reusable and flexible enough):
#implementation TestView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.clipsToBounds = YES;
self.opaque = NO;
}
return self;
}
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineWidth = 2.0f;
path.lineJoinStyle = kCGLineJoinRound;
[[UIColor blackColor] setStroke];
[[UIColor redColor] setFill];
CGFloat arcRadius = 5.0f;
CGFloat padding = 5.0f;
CGFloat triangleSide = 20.0f;
CGFloat triangleHeight = ((triangleSide * sqrtf(3.0f)) / 2);
CGFloat rectHeight = CGRectGetHeight(rect) - triangleHeight - padding - (arcRadius * 2);
// Top left corner
[path addArcWithCenter:CGPointMake(arcRadius + padding, arcRadius + padding)
radius:arcRadius
startAngle:DEGREES_TO_RADIANS(180)
endAngle:DEGREES_TO_RADIANS(270)
clockwise:YES];
// Top edge
[path addLineToPoint:CGPointMake(CGRectGetWidth(rect) - arcRadius - padding, path.currentPoint.y)];
// Top right corner
[path addArcWithCenter:CGPointMake(path.currentPoint.x, path.currentPoint.y + arcRadius)
radius:arcRadius
startAngle:DEGREES_TO_RADIANS(270)
endAngle:DEGREES_TO_RADIANS(0)
clockwise:YES];
// Right edge
[path addLineToPoint:CGPointMake(CGRectGetWidth(rect) - padding, rectHeight + padding)];
// Bottom right corner
[path addArcWithCenter:CGPointMake(path.currentPoint.x - arcRadius, path.currentPoint.y)
radius:arcRadius
startAngle:DEGREES_TO_RADIANS(0)
endAngle:DEGREES_TO_RADIANS(90)
clockwise:YES];
// Bottom edge (right part)
[path addLineToPoint:CGPointMake((CGRectGetWidth(rect) / 2) + (triangleSide / 2), path.currentPoint.y)];
// Triangle
[path addLineToPoint:CGPointMake(path.currentPoint.x - (triangleSide / 2), path.currentPoint.y + triangleHeight)];
[path addLineToPoint:CGPointMake(path.currentPoint.x - (triangleSide / 2), path.currentPoint.y - triangleHeight)];
// Bottom edge (left part)
[path addLineToPoint:CGPointMake(padding + arcRadius, path.currentPoint.y)];
// Bottom left corner
[path addArcWithCenter:CGPointMake(path.currentPoint.x, path.currentPoint.y - arcRadius)
radius:arcRadius
startAngle:DEGREES_TO_RADIANS(90)
endAngle:DEGREES_TO_RADIANS(180)
clockwise:YES];
[path closePath];
[path stroke];
[path fill];
}
#end
I want to create a progress bar that has the form of an arc. The color of the progress bar has to change according to the values.
I created an arc using UIBezierPath bezierPathWithArcCenter. I used the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:60.0 endAngle:0.0 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 15;
[self.view.layer addSublayer:arc];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
The result looks like this:
My question is: How to apply a gradient to the color if i.e. the value is <= 50%? I also created an UIButton that generates random CGFloat numbers in order to hook it up with the progress bar. Does anyone have an idea how to tackle this?
The gradient would look something like this:
Any help would be appreciated!
Thank you very much.
Granit
You can use a CAGradientLayer to get the gradient effect, and use the CAShapeLayer as a mask.
e.g.:
- (void)viewDidLoad
{
[super viewDidLoad];
int radius = 100;
CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:60.0 endAngle:0.0 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 15;
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor blueColor].CGColor ];
gradientLayer.startPoint = CGPointMake(0,0.5);
gradientLayer.endPoint = CGPointMake(1,0.5);
[self.view.layer addSublayer:gradientLayer];
//Using arc as a mask instead of adding it as a sublayer.
//[self.view.layer addSublayer:arc];
gradientLayer.mask = arc;
}
To draw a gradient along a stroke, see this implementation: https://stackoverflow.com/a/43668420/917802
EDIT:
In short, create a custom UIView class, add a radial gradient to it by iterating between two colours at increasing angles, e.g. colour1 = 1 degree, colour2 = 2 degrees etc, all the way up to 360. Then apply a donut mask to that. As you change the strokeEnd value of the masking CAShapeLayer, you also rotate the underlying radial gradient. Because they move together it looks like the stroke itself has a gradient.
The Answers given so far are great, but they are a bit complicated, I think the following logic should be much simpler:
Draw your curve/path of your shape layer .
Close the path on itself, i.e draw the same curve/path but in reverse.
Create a CAShapeLayer and set its path to the path in (2).
Give the shape layer in (3) an arbitrary stroke colour.
Create a CAGradientLayer.
Set the gradient layer mask to the shape layer in (4).
Set the colours of the gradient layer to your desired array.
Add the gradient layer to your original shapeLayer in (1).
func draweCurve(fromPoint: CGPoint, toPoint: CGPoint, x: CGFloat) {
// ------- 1 --------
let curveLayer = CAShapeLayer()
curveLayer.contentsScale = UIScreen.main.scale
curveLayer.frame = CGRect(origin: .zero, size: CGSize(width: 100, height: 100))
curveLayer.fillColor = UIColor.red.cgColor
curveLayer.strokeColor = UIColor.blue.cgColor
let path = UIBezierPath()
path.move(to: fromPoint)
let controlPoint1 = CGPoint(x: fromPoint.x + 40 * 0.45, y: fromPoint.y)
let controlPoint2 = CGPoint(x: toPoint.x - 40 * 0.45, y: toPoint.y)
path.addCurve(to: toPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
curveLayer.path = path.cgPath
// ------- 2 --------
// close the path on its self
path.addCurve(to: fromPoint, controlPoint1: controlPoint2, controlPoint2: controlPoint1)
addGradientLayer(to: curveLayer, path: path)
}
private func addGradientLayer(to layer: CALayer, path: UIBezierPath) {
// ------- 3 --------
let gradientMask = CAShapeLayer()
gradientMask.contentsScale = UIScreen.main.scale
// ------- 4 --------
gradientMask.strokeColor = UIColor.white.cgColor
gradientMask.path = path.cgPath
// ------- 5 --------
let gradientLayer = CAGradientLayer()
// ------- 6 --------
gradientLayer.mask = gradientMask
gradientLayer.frame = layer.frame
gradientLayer.contentsScale = UIScreen.main.scale
// ------- 7 --------
gradientLayer.colors = [UIColor.gray.cgColor, UIColor.green.cgColor]
// ------- 8 --------
layer.addSublayer(gradientLayer)
}