2

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.

Kex
  • 8,023
  • 9
  • 56
  • 129

1 Answers1

2

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

Jaydeep Vyas
  • 4,411
  • 1
  • 18
  • 44
  • Thanks for this. I was forgetting to change the `fromValue`. However with this solution a new animation is added every time to the layer. because `isRemovedOnCompletion` is set to `false`. Wouldn't this affect performance in a negative way? – Kex Jul 10 '19 at 05:15
  • you can optimize code by initializing `animateStroke` inside setupCircle method so it will be initialized only once and it will only update value (see updated code). I don't think it will affect performance. – Jaydeep Vyas Jul 10 '19 at 05:40