I need to animate a circle's progress when the user taps a button. I am currently using a CAShapeLayer
and CABasicAnimation
. Code like so:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var circleContainerView: UIView!
var progressPts = [0.1, 0.3, 0.7, 0.5]
let circleLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
setupCircle()
}
@IBAction func didTapAnimate(_ sender: Any) {
guard let pt = progressPts.first else { return }
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.toValue = pt
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
progressPts.removeFirst()
}
private func setupCircle() {
circleLayer.frame = CGRect(origin: .zero, size: circleContainerView.bounds.size)
let center = CGPoint(x: circleLayer.bounds.width / 2.0,
y: circleLayer.bounds.height / 2.0)
circleLayer.path = UIBezierPath(arcCenter: center,
radius: (circleLayer.bounds.height / 2.0) - 5.0,
startAngle: 0,
endAngle: 2 * CGFloat.pi, clockwise: true).cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineWidth = 10.0
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.strokeEnd = 0.0
circleContainerView.layer.addSublayer(circleLayer)
}
}
For the stroke to stay in the correct position when the animation ends I needed to set the fillMode
too .forwards
and isRemovedOnCompletion
to false
. This works fine for the first animation. However on the next animation the stoke first resets:
What am I doing wrong here? How can I animate each new progress position without a reset? Also, surely it's a bad idea setting isRemovedOnCompletion
to false
because the layer will end up with multiple animations attached.
I have just use from value property to complete circle without reset and it will animate smoothly just use below code
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var circleContainerView: UIView!
var progressPts = [0.1, 0.3, 0.7, 0.5]
var previousPts = 0.0
let circleLayer = CAShapeLayer()
var animateStroke: CABasicAnimation!
override func viewDidLoad() {
super.viewDidLoad()
setupCircle()
}
@IBAction func didTapAnimate(_ sender: Any) {
guard let pt = progressPts.first else { return }
animateStroke.fromValue = previousPts
animateStroke.toValue = pt
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
previousPts = progressPts.removeFirst()
}
private func setupCircle() {
animateStroke = CABasicAnimation(keyPath: "strokeEnd")
circleLayer.frame = CGRect(origin: .zero, size: circleContainerView.bounds.size)
let center = CGPoint(x: circleLayer.bounds.width / 2.0,
y: circleLayer.bounds.height / 2.0)
circleLayer.path = UIBezierPath(arcCenter: center,
radius: (circleLayer.bounds.height / 2.0) - 5.0,
startAngle: 0,
endAngle: 2 * CGFloat.pi, clockwise: true).cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineWidth = 10.0
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.strokeEnd = 0.0
circleContainerView.layer.addSublayer(circleLayer)
}
}
OUTPUT