Search code examples
swiftcore-animationcashapelayercabasicanimationios-animations

Pulsing Animation on layer


I'm making some custom type of loader for an app where I just simply make 4 circular layers inside my view and it all set and display fine. but when I apply pulsing animation scaling of all layers they disturb all my design and layer scaling fine but it's change it's center point. I wants all circular layers first scaling the layer and make small it's size and make it large then again identity and it should call infinity. but should not change it's center point.

here's I'm sharing my code

class ViewController: BaseViewController{
    
    @IBOutlet weak var layerView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        drawCircles()
    }

    func drawCircles(){
        let width = layerView.bounds.width
        let halfOfView = width / 2
        
        let firstCirleLayer = UIBezierPath(arcCenter: CGPoint(x: halfOfView / 2, y: halfOfView), radius: halfOfView /  3.5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        circleLayers(path: firstCirleLayer.cgPath, color: UIColor.purple)
        
        let secondCirleLayer = UIBezierPath(arcCenter: CGPoint(x: halfOfView, y: halfOfView / 2), radius: halfOfView /  3.5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        circleLayers(path: secondCirleLayer.cgPath, color: UIColor.yellow)
        
        let thirdCirleLayer = UIBezierPath(arcCenter: CGPoint(x: width * 0.75, y: halfOfView), radius: halfOfView /  3.5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        circleLayers(path: thirdCirleLayer.cgPath, color: UIColor.brown)

        let fourthCirleLayer = UIBezierPath(arcCenter: CGPoint(x: halfOfView, y: width * 0.75), radius: halfOfView /  3.5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        circleLayers(path: fourthCirleLayer.cgPath, color: UIColor.blue)
    }
    
    func circleLayers(path: CGPath, color: UIColor){

        let layer = CAShapeLayer()
        layer.path = path
        layer.fillColor = color.cgColor
        layerView.layer.addSublayer(layer)
        
        let scaling = CABasicAnimation(keyPath: "transform.scale")
        scaling.toValue = 1.2
        scaling.duration = 0.3
        scaling.autoreverses = true
        scaling.repeatCount = .infinity
        layer.add(scaling, forKey: nil)
    }
    
}

Solution

  • To scale each circle from their center, you should set the frame of each layer to the bezier path's bounding box. Then you can just set a constant origin of CGPoint(x: radius, y: radius) for the bezier path's arcCenter.

    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            drawCircles()
        }
    
        func drawCircles(){
            let width = layerView.bounds.width
            let halfOfView = width / 2
            let radius = halfOfView / 3.5
    
            let firstCenter = CGPoint(x: halfOfView / 2, y: halfOfView)
            makeCircleLayer(center: firstCenter, radius: radius, color: .purple)
    
            let secondCenter = CGPoint(x: halfOfView, y: halfOfView / 2)
            makeCircleLayer(center: secondCenter, radius: radius, color: .yellow)
    
            let thirdCenter = CGPoint(x: width * 0.75, y: halfOfView)
            makeCircleLayer(center: thirdCenter, radius: radius, color: .brown)
    
            let fourthCenter = CGPoint(x: halfOfView, y: width * 0.75)
            makeCircleLayer(center: fourthCenter, radius: radius, color: .blue)
    
        }
    
        func makeCircleLayer(center: CGPoint, radius: CGFloat, color: UIColor) {
            
            let layer = CAShapeLayer()
    
            /// the frame is the actual frame/bounding box of the circle
            layer.frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
    
            /// path is relative to the frame
            let path = UIBezierPath(arcCenter: CGPoint(x: radius, y: radius), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
            layer.path = path.cgPath
    
            layer.fillColor = color.cgColor
            layerView.layer.addSublayer(layer)
    
            let scaling = CABasicAnimation(keyPath: "transform.scale")
            scaling.toValue = 1.2
            scaling.duration = 0.3
            scaling.autoreverses = true
            scaling.repeatCount = .infinity
            layer.add(scaling, forKey: nil)
        }
    }
    

    Result:

    Each circle scales from its center

    However, because your circles are just circles (not a complicated shape), you should just use UIView's with a corner radius. The following code produces the same result, but is much cleaner.

    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            drawCircles()
        }
    
        func drawCircles() {
            let width = layerView.bounds.width
            let halfOfView = width / 2
            let radius = halfOfView / 3.5
    
            let firstCenter = CGPoint(x: halfOfView / 2, y: halfOfView)
            makeCircleView(center: firstCenter, radius: radius, color: .purple)
    
            let secondCenter = CGPoint(x: halfOfView, y: halfOfView / 2)
            makeCircleView(center: secondCenter, radius: radius, color: .yellow)
    
            let thirdCenter = CGPoint(x: width * 0.75, y: halfOfView)
            makeCircleView(center: thirdCenter, radius: radius, color: .brown)
    
            let fourthCenter = CGPoint(x: halfOfView, y: width * 0.75)
            makeCircleView(center: fourthCenter, radius: radius, color: .blue)
        }
    
        func makeCircleView(center: CGPoint, radius: CGFloat, color: UIColor) {
            let frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
            let circleView = UIView(frame: frame)
            circleView.backgroundColor = color
            circleView.layer.cornerRadius = radius
            layerView.addSubview(circleView)
            
            UIView.animate(withDuration: 0.3, delay: 0, options: [.repeat, .autoreverse]) {
                circleView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
            }
        }
    }