iosswiftcaanimation

CAShapeLayer circle strokeEnd not connecting to start


I'm creating a circle similar to the Android material design loading indicator seen on the right here, but the circle isn't completing itself.

class ViewController: UIViewController {

    var baseLayer = CAShapeLayer()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUp()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        animate()
    }

    private func setUp() {
        view.layer.addSublayer(baseLayer)
        let basePath = UIBezierPath(arcCenter: view.center,
                                    radius: 100,
                                    startAngle: CGFloat.pi / 2,
                                    endAngle: 2 * CGFloat.pi,
                                    clockwise: true).cgPath
        baseLayer.strokeColor = UIColor.gray.cgColor
        baseLayer.path = basePath
        baseLayer.fillColor = UIColor.clear.cgColor
        baseLayer.lineWidth = 2
        baseLayer.position = view.center
        baseLayer.strokeEnd = 0
    }

    private func animate() {
        CATransaction.begin()
        let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
        strokeEndAnimation.toValue = 1
        strokeEndAnimation.fillMode = .forwards
        strokeEndAnimation.isRemovedOnCompletion = false
        strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

        let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotationAnimation.toValue = CGFloat.pi
        rotationAnimation.fillMode = .forwards
        rotationAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        rotationAnimation.isRemovedOnCompletion = false

        let group = CAAnimationGroup()
        group.duration = 1.5
        group.repeatCount = 0
        group.fillMode = .forwards
        group.isRemovedOnCompletion = false
        group.animations = [strokeEndAnimation, rotationAnimation]

        baseLayer.add(group, forKey: "animations")
        CATransaction.commit()
    }
}

The image below shows what I'm talking about. It's correctly stopping at 3*pi/2 but I believe the strokeEnd is incorrectly stopping at pi. I tried various different configurations of strokeEnd and transform animations, but nothing seems to work. Modifying the strokeEnd toValue animation doesn't change anything.

Gif of issue


Solution

  • PI is one-half of the circle. If you remove the rotation, and use this as your basePath:

        basePath = UIBezierPath(arcCenter: view.center,
                                radius: 100,
                                startAngle: 0,
                                endAngle: 1.0 * CGFloat.pi,
                                clockwise: true).cgPath
    

    enter image description here

    The line will start at 3:00 and go to 9:00

    If you start at one-half PI:

        basePath = UIBezierPath(arcCenter: view.center,
                                radius: 100,
                                startAngle: CGFloat.pi / 2,
                                endAngle: 1.0 * CGFloat.pi,
                                clockwise: true).cgPath
    

    enter image description here

    Your line goes from 6:00 to 9:00

    Add a half-PI to the endAngle:

        basePath = UIBezierPath(arcCenter: view.center,
                                radius: 100,
                                startAngle: CGFloat.pi / 2,
                                endAngle: 1.5 * CGFloat.pi,
                                clockwise: true).cgPath
    

    enter image description here

    and you get 6:00 to 12:00, which is almost what you want.

    Now you add a rotation of PI (remember, that's one-half of a full circle):

    enter image description here

    and you're at 12:00 to 6:00.

    To bring the end up to 12:00, gotta add another PI

        var basePath = UIBezierPath(arcCenter: view.center,
                                    radius: 100,
                                    startAngle: CGFloat.pi / 2,
                                    endAngle: 2.5 * CGFloat.pi,
                                    clockwise: true).cgPath
    

    enter image description here