Swift: How to set CAShapeLayer() progress value - ios

I just create an IOS app where I implement a round progress bar with CAShapeLayer().
And know I want to set in each second the progress value to +1 is this posible?
I would just like to create a progress bar like this one that leaps forward a value every second:
Like this
Best Regards!

CAShapeLayer has animatable strokeStart and strokeEnd properties. You can directly set these and they will be animated automatically, or you can control the animation yourself like any other animatable CALayer property:
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
This will fill the progress indicator in 1 second.
For a stroke end animation to work, you can't use a CGPath which is just a circle - you need an arc with a start and an end, like this:
let path = CGMutablePath()
path.addArc(
center: CGPoint(x: 150, y: 150),
radius: 100,
startAngle: -0.5 * .pi,
endAngle: 1.5 * .pi,
clockwise: false
)

Related

UIView pauses during animation along UIBezierPath

I'm trying to animate a UIView(a square) to move along a UIBezierPath using a CAKeyframeAnimation. The square pauses at two points along the bezier path, both points being right before the path begins to arc.This is my code for the UIBezierPath and Animation:
func createTrack() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: layerView.frame.size.width/2, y: 0.0))
path.addLine(to: CGPoint(x: layerView.frame.size.width - 100.0, y: 0.0))
path.addArc(withCenter: CGPoint(x: layerView.frame.size.width - 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(270).toRadians(), endAngle: CGFloat(90).toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: 100.0, y: layerView.frame.size.height))
path.addArc(withCenter: CGPoint(x: 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(90).toRadians(), endAngle: CGFloat(270).toRadians(), clockwise: true)
path.close()
return path
}
#IBAction func onAnimatePath(_ sender: Any) {
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
square.backgroundColor = UIColor.red
layerView.addSubview(square)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.repeatCount = Float.infinity
animation.duration = 60
square.layer.add(animation, forKey: "animate position along path")
}
layerView is just a UIView. Any ideas on why this happens and how I can fix this?
The path you are using consists of 5 segments (two arcs and three lines (including the one when you close the path)) and the animation spends the same time on each of the segments. If this path if too narrow, these lines segments will have no length and the square will appear still for 12 seconds in each of them.
You probably want to use a "paced" calculation mode to achieve a constant velocity
animation.calculationMode = kCAAnimationPaced
This way, the red square will move at a constant pace – no matter how long each segment of the shape is.
This alone will give you the result you are after, but there's more you can do to simplify the path.
The addArc(...) method is rather smart and will add a line from the current point to the start of the arc itself, so the two explicit lines aren't needed.
Additionally, if you change the initial point you're moving the path to to have the same x component as the center of the second arc, then the path will close itself. Here, all you need are the two arcs.
That said, if the shape you're looking to create is a rounded rectangle like this, then you can use the UIBezierPath(roundedRect:cornerRadius:) convenience initializer:
let radius = min(layerView.bounds.width, layerView.bounds.height) / 2.0
let path = UIBezierPath(roundedRect: layerView.bounds, cornerRadius: radius)

How to use UIViewAnimationCurve options in CAAnimation

I am using a CAAnimation in a CAShapeLayer to simply animate a line that strokes from left to right (it's just a cut-down version of a more complex program). I have slowed it down to show that the beginning quarter is almost instantaneous, and then the rest animates normally.
I have seen that one can use UIView.animate's options to set the UIViewAnimationCurve options (for instance, .EaseIn or .Linear). Is it possible to set these options when embedding a CAAnimation in a CAShapeLayer? And is it necessary to create the animation every time you want to run it, or is there a "run animation" command?
Below is my cut-down code:
// the containing UIView is called mainView
mainView.layer.sublayers = nil
// create a line using a UIBezierPath
let path = UIBezierPath()
path.move(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 300, y: 100))
// create a CAShapeLayer
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: 1024, height: 1024)
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 100
shapeLayer.path = path.cgPath
// create the animation
mainView.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "LetterStroke")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 10
shapeLayer.add(animation, forKey: "animation")
Many thanks in advance.
You should use the timingFunction property of CABasicAnimation. There is a constructor that accepts these predefined timing functions like
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
The problem was that the line animation appeared to start a quarter of the way in. I neglected to put what turned out to be the offending line of code in my question.
shapeLayer.lineCap = kCALineCapRound
This, combined with the lineWidth of 100, made the line start approximately a quarter of the way from the beginning. I made the lineWidth smaller, and it worked fine.

CAKeyFrameAnimation not Linear for values greater than PI

I am having some trouble to understand why an animation isn't working like expected.
What I am doing is this:
Create a UIBezierPath with an arc to move a Label along this path and animate the paths stroke.
//Start Point is -.pi /2 to let the Arc start at the top.
//self.progress = Value between 0.0 and 1.0
let path : UIBezierPath = UIBezierPath.init(arcCenter: CGPoint.init(x: self.bounds.width * 0.5, y: self.bounds.height * 0.5),
radius: self.bounds.width * 0.5, startAngle: -.pi / 2, endAngle: (2 * self.progress * .pi) - (.pi / 2), clockwise: true)
return path
Add this path to a CAShapeLayer
circlePathLayer.frame = bounds
circlePathLayer.path = self.path.cgPath
circlePathLayer.strokeStart = 0
circlePathLayer.strokeEnd = 1
Animate the strokeEnd property with a CABasicAnimation
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.repeatCount = HUGE
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = self.animationDuration
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
Animate the position property of my label with a CAKeyFrameAnimation
let animationScore = CAKeyframeAnimation(keyPath: "position")
//some things I tried to fix
//animationScore.timingFunctions = [CAMediaTimingFunction(controlPoints: 0.250, 0.250, 0.750, 0.750)]
//animationScore.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionLinear)
animationScore.path = self.path.cgPath
animationScore.duration = self.animationDuration
animationScore.isRemovedOnCompletion = false
animationScore.fillMode = kCAFillModeBoth
animationScore.repeatCount = HUGE
Add my animations to layer and label
self.circlePathLayer.add(animation, forKey: nil)
self.scoreLabel.layer.add(animationScore, forKey: nil)
My Problem: For ProgressValues greater than 0.75 my label is not moving in linear speed. Values greater than 0.75 mean that my arc is greater than PI.
For values less than 0.75 my animation works fine and label and strokeend have the same speed and are on top of each other.
GIF :
Please ignore the 100% in the Label in this gif my progress was at a value of 0.76.
You see my Label slows down after three quarters of my circle.
I hope someone can help me.
Many thanks
The keyframe animation introduces an unnecessary complication. Simply rotate the label around the center with the same duration as the shape layer's stroke animation:
(I apologize that my animation starts at the bottom, not the top, but I wasn't looking at your question when I wrote the code and now I'm too lazy to change it!)
So, how is that done? It's three animations, all with the same duration:
The shape layer's strokeEnd, like your animation.
An "arm" running thru the center of the circle, with the label as a sublayer at one end (so that the label appears at the radius of the circle). The arm does a rotation transform animation.
The label does a rotation transform animation in the opposite direction. If it didn't, it would rotate along with its superlayer. (Think of how a Ferris wheel works; your chair is on the end of the arm, but it remains upright with respect to the earth.)
This is the entire animation code:
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.fromValue = 0
anim.toValue = 5
anim.duration = 10
self.arm.layer.add(anim, forKey:nil)
let anim2 = CABasicAnimation(keyPath: "transform.rotation.z")
anim2.fromValue = 0
anim2.toValue = -5
anim2.duration = 10
self.lab.layer.add(anim2, forKey:nil)
let anim3 = CABasicAnimation(keyPath: "strokeEnd")
anim3.fromValue = 0
anim3.toValue = 1
anim3.duration = 10
self.shape.add(anim3, forKey:nil)

Strange behaviour when animating between two circular UIBezierPaths

The Problem
I am creating an exploding rings effect. I'm using multiple CAShapeLayer's and creating a UIBezierPath for each layer. When the view is initialised, all the layers have a path with width 0. When an action is triggered, the rings animate to a larger path.
As seen in the demo below, the right hand edge of each layer is slower to animate than the remainder of each circular layer.
Demo
Code
Drawing the layers:
func draw(inView view: UIView) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
// size.width defaults to 0
let circlePath = UIBezierPath(arcCenter: view.center, radius: size.width / 2, startAngle: 0, endAngle: CGFloat(360.0).toRadians(), clockwise: true)
shapeLayer.path = circlePath.CGPath
shapeLayer.frame = view.frame
return shapeLayer
}
Updating the layers
func updateAnimated(updatedOpacity: CGFloat, updatedSize: CGSize, duration: CGFloat) {
let updatePath = UIBezierPath(arcCenter: view.center, radius: updatedSize.width / 2,
startAngle: 0, endAngle: CGFloat(360).toRadians(), clockwise: true)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = layer.path // the current path (initially with width 0)
pathAnimation.toValue = updatePath.CGPath
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = self.opacity
opacityAnimation.toValue = updatedOpacity
let animationGroup = CAAnimationGroup()
animationGroup.animations = [pathAnimation, opacityAnimation]
animationGroup.duration = Double(duration)
animationGroup.fillMode = kCAFillModeForwards
animationGroup.removedOnCompletion = false
layer.addAnimation(animationGroup, forKey: "shape_update")
... update variables ...
}
Avoid starting with a path width of 0. It is very difficult to scale from 0 and tiny floating-point errors magnify. Values very close to zero are not very continuous in floating point (hmmm.... I guess that actually is a little like the real universe). Try starting with larger values and see how close to zero you can go safely.

How to make a filling progress circle in iOS?

I need to make a progress circle like this:
I have created a CAShapeLayer for the circle, and animate its strokeEnd property like this:
pieShape = CAShapeLayer()
pieShape.path = UIBezierPath(arcCenter: CGPointMake(29, 29), radius: 27.0, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true).CGPath
pieShape.fillColor = UIColor.clearColor().CGColor
pieShape.strokeColor = UIColor.blackColor().CGColor
pieShape.lineWidth = 4.0
self.layer.addSublayer(pieShape)
let countDownAnimation = CABasicAnimation(keyPath: "strokeEnd")
countDownAnimation.duration = durationInSeconds
countDownAnimation.removedOnCompletion = false
countDownAnimation.fromValue = NSNumber(float: 0.0)
countDownAnimation.toValue = NSNumber(float: 1.0)
countDownAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
self.pieShape.strokeStart = 1.0
self.pieShape.addAnimation(countDownAnimation, forKey: "drawCircleAnimation")
But, this not what I want. I need to animate the circle filling, not only the border. Thank you.
The easiest way is to cheat, and animate just the stroke, but make it appear to be animating the fill of the circle. For example, if you wanted a circle of radius 30, use a radius of 15 instead, but a lineWidth of 30, so that the stroke effectively covers from the center to the desired radius of 30.
There are more complicated methods using CADisplayLink, and updating it yourself, but the above is pretty easy way to achieve the desired effect.

Resources