iosswiftuibezierpathcgaffinetransformcabasicanimation

CAShapeLayer path animation shrinking before completing animation


I am trying to animate a rotation of a layer's CGpath.

The rotation works perfectly, but for some reason, the path seems to shrink, then get bigger again. I just want it to show a rotating animation. I don't want it to shrink

here is a video.

here is the code

class ViewController: UIViewController {

@IBOutlet weak var overlayView: UIView!


override func viewDidLoad() {
    super.viewDidLoad()
    
    
    let maskLayer = CAShapeLayer()
    let path = UIBezierPath(rect: view.bounds)
    let mask = UIBezierPath.drawSquare(width: 100, center: view.center)
    
    // Makes it so we can see through the center of the view
    maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
    path.append(mask)
    maskLayer.path = path.cgPath
    overlayView.layer.mask = maskLayer
}

@IBAction func rotateView(_ sender: Any) {
    
    let maskPath = UIBezierPath.drawSquare(width: 100, center: overlayView.center)
    let path = UIBezierPath(rect: overlayView.frame)
    
    let bounds: CGRect = maskPath.cgPath.boundingBox
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    
    let radians = 90 / 180.0 * .pi
    var transform: CGAffineTransform = .identity
    transform = transform.translatedBy(x: center.x, y: center.y)
    transform = transform.rotated(by: radians)
    transform = transform.translatedBy(x: -center.x, y: -center.y)
    maskPath.apply(transform)

    path.append(maskPath)
    
    // create new animation
    let anim = CABasicAnimation(keyPath: "path")
    anim.toValue = path.cgPath
    
    (overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "path")

}

}

extension UIBezierPath{

static func drawSquare(width: CGFloat, center: CGPoint) -> UIBezierPath {
    let rect = CGRect(x: center.x - width/2, y: center.y - width/2, width: width, height: width)

    let path = UIBezierPath()
    path.move(to: rect.origin)
    
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
    path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
    path.close()
    return path
}
}

Solution

  • Your "cutout box" is shrinking and growing because you're using path animation.

    When you animate a path from one set of points to another, each point will take a straight line from its starting point to its ending point.

    Couple ways to visualize this...

    First, we'll add an "outline" frame to the cutout mask box:

    enter image description here

    as you see, each corner is taking a direct route to its next position.

    If we highlight one side, it's maybe a bit more obvious:

    enter image description here

    and, we can number the corners for even more clarity:

    enter image description here

    So, if we want to rotate the square without changing its size, we could use keyframe animation and run the corners along a circle:

    enter image description here enter image description here

    or, much easier, rotate the mask layer instead of its path:

    let radians = 90 / 180.0 * .pi
    let anim = CABasicAnimation(keyPath: "transform.rotation.z")
    anim.toValue = radians
    anim.duration = 1.0
    (overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "layerRotate")
    

    enter image description here

    No doubt we notice that the mask layer frame does not completely cover the overlayView frame.

    So, we can fix that by making the mask layer frame larger.

    We could calculate exactly how big it needs to be, but because shape layers are very efficient, we'll just make it a square, 1.5 times bigger than the longer axis:

    enter image description here

    so when it rotates, it covers the view:

    enter image description here

    and we finish with this:

    enter image description here