Loading Indicator using CABasicAnimation

68 Views Asked by At

How can I make this loading indicator using CABasicAnimation? I was trying to do like this

like this

but it's working not correctly, I am not sure that adding four layers with specific paths and animations is a good idea. I need to make exactly the same I see on the picture. I hope, it's doable using only one layer with some magic

    private func setupView() {
        let firstLayer = createLayer()
        let secondLayer = createLayer()
        let thirdLayer = createLayer()
        let fourthLayer = createLayer()

        firstLayer.path = createBezierPath(
            layerWidth: firstLayer.lineWidth, 
            startAngle: 0, 
            endAngle: 0.25
        ).cgPath
        
        secondLayer.path = createBezierPath(
            layerWidth: secondLayer.lineWidth, 
            startAngle: 0.25, 
            endAngle: 0.5
        ).cgPath

        thirdLayer.path = createBezierPath(
            layerWidth: thirdLayer.lineWidth, 
            startAngle: 0.5, 
            endAngle: 0.75
        ).cgPath
        
        fourthLayer.path =  createBezierPath(
            layerWidth: thirdLayer.lineWidth, 
            startAngle: 0.75, 
            endAngle: 1
        ).cgPath
        
        firstLayer.add(createAnimation(), forKey: "firstLayer")
        secondLayer.add(createAnimation(), forKey: "secondLayer")
        thirdLayer.add(createAnimation(), forKey: "thirdLayer")
        fourthLayer.add(createAnimation(), forKey: "thirdLayer")

        self.layer.addSublayer(firstLayer)
        self.layer.addSublayer(secondLayer)
        self.layer.addSublayer(thirdLayer)
        self.layer.addSublayer(fourthLayer)
        
        
        let animation = CABasicAnimation(keyPath: "transform.rotation.z")
        animation.beginTime = 0
        animation.duration = 1
        animation.fromValue = CGFloat.angle(progress: 0)
        animation.toValue = CGFloat.angle(progress: 0.25)
        animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
        animation.fillMode = .forwards
        animation.repeatDuration = .infinity
        
        self.layer.add(animation, forKey: "mainAnim")
    }
    
    private func createBezierPath(
        layerWidth: CGFloat,
        startAngle: CGFloat,
        endAngle: CGFloat
    ) -> UIBezierPath {
        return .init(
            arcCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2), 
            radius: (bounds.height - layerWidth) / 2, 
            startAngle: .angle(progress: startAngle), 
            endAngle: .angle(progress: endAngle), 
            clockwise: true
        )
    }
    
    private func createAnimation() -> CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.beginTime = 0
        animation.duration = 1
        animation.fromValue = 0
        animation.toValue = 1
        animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        animation.repeatDuration = .infinity
        return animation
    }
    
    private func createLayer() -> CAShapeLayer {
        let shapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = UIColor.orange.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 5
        shapeLayer.lineCap = .round
        shapeLayer.strokeEnd = 0
        return shapeLayer
    }

private extension CGFloat {
    static var initialAngle: CGFloat = -(.pi / 2)

    static func angle(progress: CGFloat) -> CGFloat {
        .pi * 2 * progress + .initialAngle
    }
}
1

There are 1 best solutions below

0
rob mayoff On

I'd use a motion design tool to recreate that. For example, I think you could create this animation with the free version of Flow, which can export Swift code or Lottie files.