Search code examples
iosswiftuibezierpathcashapelayercakeyframeanimation

iOS - Spinner to a circular view


I am trying to add spinner effect to a circular view.

    func spin() {
        let circlePathLayer = CAShapeLayer()
        circlePathLayer.fillColor = nil
        circlePathLayer.strokeColor = UIColor.redColor.cgColor
        circlePathLayer.lineWidth = 6
        layer.addSublayer(circlePathLayer)
        setPath(to: circlePathLayer)
        animate(layer: circlePathLayer)
    }
    
    private func setPath(to layer: CAShapeLayer) {
        layer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: layer.lineWidth / 2,
                                                                   dy: layer.lineWidth / 2)).cgPath
    }
    
    private func animate(layer: CAShapeLayer) {
        animateKeyPath(keyPath: "strokeEnd",
                       duration: 1,
                       values: [0, 1],
                       layer: layer)
        animateKeyPath(keyPath: "strokeStart",
                       duration: 1,
                       values: [(0 - 0.1) as CGFloat, (1 - 0.1) as CGFloat],
                       layer: layer)
    }
    
    private func animateKeyPath(keyPath: String, duration: CFTimeInterval, values: [CGFloat], layer: CAShapeLayer) {
        let animation = CAKeyframeAnimation(keyPath: keyPath)
        animation.values = values
        animation.calculationMode = .linear
        animation.duration = duration
        animation.repeatCount = Float.infinity
        layer.add(animation, forKey: animation.keyPath)
    }

But for the above code, at the end of animation duration, a flicker happens every time at 1 position of the strokeEnd. How could I overcome the flicker and regulate the spinner?

Just to notice the effect visually, I have set the duration to 5 seconds. Please notice when the spinner reaches 3'o clock position.

enter image description here


Solution

  • I think strokeStart and strokeEnd are constrained to be between 0 and 1. When you try to set strokeStart to a negative number (like -0.1) the system pins it to zero.

    Try this solution:

    //: A UIKit based Playground for presenting user interface
    
    import UIKit
    import PlaygroundSupport
    
    class SpinnerView : UIView {
        let circlePathLayer = CAShapeLayer()
    
        override func didMoveToSuperview() {
            layer.addSublayer(circlePathLayer)
        }
    
        func spin() {
            let circleFrame = bounds.insetBy(dx: -40, dy: -40)
            circlePathLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
            
            circlePathLayer.strokeColor = UIColor.red.cgColor
            circlePathLayer.fillColor = nil
            circlePathLayer.lineWidth = 6
    
            setPath(to: circlePathLayer)
            animate(layer: circlePathLayer)
        }
    
        private func setPath(to layer: CAShapeLayer) {
            let path = UIBezierPath()
            path.addArc(withCenter: CGPoint(x: layer.bounds.midX,
                                            y: layer.bounds.midY),
                        radius: (bounds.width / 2.0) - layer.lineWidth / 2,
                        startAngle: 0,
                        endAngle: 2 * .pi / 10.0,
                        clockwise: true)
            layer.path = path.cgPath
        }
    
        private func animate(layer: CAShapeLayer) {
            let animation = CABasicAnimation(keyPath: "transform.rotation")
            animation.duration = 1
            animation.repeatCount = Float.infinity
            animation.fromValue = 0
            animation.toValue = 2 * CGFloat.pi
            layer.add(animation, forKey: "transform.rotation")
        }
    }
    
    class MyViewController : UIViewController {
        let spinner = SpinnerView()
    
        override func loadView() {
            let view = UIView()
            view.backgroundColor = .white
    
            spinner.translatesAutoresizingMaskIntoConstraints = false
    
            view.addSubview(spinner)
    
            view.addConstraints([
                NSLayoutConstraint(item: spinner,
                                   attribute: .centerX,
                                   relatedBy: .equal,
                                   toItem: view,
                                   attribute: .centerX,
                                   multiplier: 1.0,
                                   constant: 0),
                NSLayoutConstraint(item: spinner,
                                   attribute: .centerY,
                                   relatedBy: .equal,
                                   toItem: view,
                                   attribute: .centerY,
                                   multiplier: 1.0,
                                   constant: 0),
                NSLayoutConstraint(item: spinner,
                                   attribute: .width,
                                   relatedBy: .equal,
                                   toItem: view,
                                   attribute: .width,
                                   multiplier: 1.0,
                                   constant: -40)
            ])
    
            spinner.addConstraints([
                NSLayoutConstraint(item: spinner,
                                   attribute: .width,
                                   relatedBy: .equal,
                                   toItem: spinner,
                                   attribute: .height,
                                   multiplier: 1.0,
                                   constant: 0)
            ])
    
            self.view = view
        }
    
        override func viewDidLayoutSubviews() {
            spinner.spin()
        }
    }
    // Present the view controller in the Live View window
    PlaygroundPage.current.liveView = MyViewController()