Search code examples
iosswiftcashapelayer

Animating a circles progress more than once with CAShapeLayer


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:

enter image description here

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.


Solution

  • 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

    enter image description here