swiftmacosanimationcalayer

How to rotate a CAShapeLayer around some point with animation?


A custom view has some CALayers as its content.

picture

I want to add a consistent rotation animation for the red line. (imagine it as pointer of an analog clock)

Expected result: the red line rotates at the center of the blue square.

Current result: the red line rotates at the left bottom corner of the blue square. See gif below.

gif


The animation code for the red line is :

let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
    
// Set animation properties
rotationAnimation.fromValue = CGFloat.pi * 2.0
rotationAnimation.toValue = 0
rotationAnimation.duration = 2.0
rotationAnimation.isCumulative = true
rotationAnimation.repeatCount = Float.greatestFiniteMagnitude
    
// Add rotation animation to pointer layer
pointerLayer.add(rotationAnimation, forKey: "rotationAnimation")

The complete project is here:

https://github.com/cool8jay/public/files/11426695/testDrawPointer.zip


Here are what I have tried after google or stackoverflow online:

  1. change anchorPoint
  2. use CATransform3D
  3. use CGAffineTransform
  4. combinations of above

None works for me.


Solution

  • You forgot to set the frame of your layers, add these lines to your code and it will works:

    borderLayer.frame = bounds
    pointerLayer.frame = borderLayer.bounds
    

    Leo's answer works, too. The reason you are seeing the red pointer stretch out the blue border is because the color and shape you use, combine with the stroke line's parallel drawing and bounds clipping, can easily create optical illusion. So it would looks like its not rotating around the center but in fact it do.

    Here are the complete code:

    class ClockView: NSView {
        
        let pointerLayer = CAShapeLayer()
        let borderLayer = CAShapeLayer()
        
        override public init(frame frameRect: NSRect) {
            super.init(frame: frameRect)
            setup()
        }
        
        required public init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
        
        private func setup() {
            self.wantsLayer = true
    
            borderLayer.frame = bounds
            borderLayer.fillColor = NSColor.gray.cgColor
            borderLayer.path = CGPath(ellipseIn: borderLayer.bounds, transform: nil)
            layer?.addSublayer(borderLayer)
            
            let path = CGMutablePath()
            path.move(to: CGPointMake(bounds.midX, bounds.midY))
            path.addLine(to: CGPointMake(bounds.midX + 50, bounds.midY))
            pointerLayer.path = path
            pointerLayer.frame = bounds
            pointerLayer.lineWidth = 8
            pointerLayer.strokeColor = NSColor.red.cgColor
            layer?.addSublayer(pointerLayer)
    
            let animation = CABasicAnimation(keyPath: "transform.rotation.z")
            animation.fromValue = CGFloat.pi * 2
            animation.toValue = 0
            animation.duration = 8
            animation.isCumulative = true
            animation.repeatCount = Float.greatestFiniteMagnitude
            pointerLayer.add(animation, forKey: "rotationAnimation")
        }
    }
    

    I change the borderLayer's shape to a circle, get rid of the line stroke, as well as changing the color to a least dazzling one.

    I also rearrange the code a bit so its easier to read.