Search code examples
iosswiftuiviewanimationuiactivityindicatorview

Custom indicator with rotate blink animation like UIActivityIndicatorView


I am trying to make custom activity indicator, see the indicator class below

import UIKit
class MyIndicator: UIView {
    let gap = CGFloat(.pi/4 / 6.0)
    var count = 0
    override func draw(_ rect: CGRect) {
        super.draw(rect)
    }
    func blink() {
        backgroundColor = .clear
        let duration: CFTimeInterval = 1.2
        //let beginTime = CACurrentMediaTime()
        let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5]
        let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        // Animation
        let animation = CAKeyframeAnimation(keyPath: "opacity")
        animation.keyTimes = [0, 0.5, 1]
        animation.timingFunctions = [timingFunction, timingFunction]
        animation.values = [1, 0.3, 1]
        animation.duration = duration
        animation.repeatCount = HUGE
        animation.isRemovedOnCompletion = false

        for i in 0...3 {
            let shape = CAShapeLayer()
            shape.frame = self.bounds
            shape.fillColor = UIColor.clear.cgColor
            shape.lineWidth = 6.8
            shape.strokeColor = UIColor.blue.cgColor

            let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap
            let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2

            shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
            animation.beginTime =  beginTimes[i]
            shape.add(animation, forKey: "animation")
            self.layer.addSublayer(shape)
        }
    }
    func startAnimating() {
        blink()
    }
}


let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        self.view.addSubview(indicator)
        indicator.startAnimating()

I have attached my current result. enter image description here

But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.


Solution

  • Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close.

    //: A UIKit based Playground for presenting user interface
    
    import UIKit
    import PlaygroundSupport
    
    
    class MyIndicator: UIView {
    
        let gap = CGFloat(.pi/4 / 6.0)
        private var replicatorLayer = CAReplicatorLayer()
        private var mainShapeLayer = CAShapeLayer()
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonSetup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonSetup()
        }
    
        func commonSetup(){
    
            mainShapeLayer = CAShapeLayer()
            mainShapeLayer.frame = self.bounds
            mainShapeLayer.fillColor = UIColor.clear.cgColor
            mainShapeLayer.lineWidth = 6.8
            mainShapeLayer.strokeColor = UIColor.blue.cgColor
    
            let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2
            let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2
    
            mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
    
    
            replicatorLayer = CAReplicatorLayer()
            replicatorLayer.frame = self.bounds
            replicatorLayer.instanceCount = 4
            let angle = (Double.pi * 2)/4
            replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1)
            replicatorLayer.addSublayer(mainShapeLayer)
            replicatorLayer.opacity = 0
    
            self.layer.addSublayer(replicatorLayer)
        }
    
    
        func animate(){
    
            let defaultDuration : Double = 0.75
    
            let animate = CAKeyframeAnimation(keyPath: "opacity")
            animate.values = [1, 0.3, 1]
            animate.keyTimes = [0, 0.5, 1]
            animate.repeatCount = .greatestFiniteMagnitude
            animate.duration = defaultDuration
            animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    
            replicatorLayer.instanceDelay = defaultDuration/4
            self.mainShapeLayer.add(animate, forKey: nil)
    
            let opacityIn = CABasicAnimation(keyPath: "opacity")
            opacityIn.fromValue = 1
            opacityIn.toValue = 0
            opacityIn.duration = 0.2
            replicatorLayer.add(opacityIn, forKey: nil)
            self.replicatorLayer.opacity = 1
        }
    
        func stopAnimating(){
            CATransaction.begin()
            let opacityOut = CABasicAnimation(keyPath: "opacity")
            opacityOut.fromValue = 1
            opacityOut.toValue = 0
            opacityOut.duration = 0.2
            CATransaction.setCompletionBlock {
                [weak self] in
                self?.mainShapeLayer.removeAllAnimations()
            }
            replicatorLayer.add(opacityOut, forKey: nil)
            self.replicatorLayer.opacity = 0
            CATransaction.commit()
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            mainShapeLayer.frame = self.bounds
            replicatorLayer.frame = self.bounds
        }
    
    }
    
    
    class MyViewController : UIViewController {
        override func loadView() {
            let view = UIView()
            view.backgroundColor = .white
    
            let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
            indicator.animate()
            //just to simulate starting and stoping
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
                indicator.stopAnimating()
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
                    indicator.animate() 
                }
            }
    
            view.addSubview(indicator)
            self.view = view
        }
    }
    // Present the view controller in the Live View window
    PlaygroundPage.current.liveView = MyViewController()