I am working on a project that will be animating polygon vector shapes. No raster images on a CAShapeLayer. In general, most of the references and examples related to CAShapeLayer are concentrated on animating stroke end points. That’s great for progress shapes where you are animating out the stroke, but what about simply going from one shape to another? How about in Swift?
Project and storyboard
Create a simple swift project. Place a view and a button on it. Set an IBOutlet for the view as shapeView, and an IBAction for the button. If you are impatient, you can download the demo project.
Create the objects
var shape1: UIBezierPath? var shape2: UIBezierPath? var isOpen: Bool = false var shapeLayer = CAShapeLayer()
We’re going to animate from shape1 (a small polygon) to shape2 (a larger polygon), where the shapes are not the same. The state between larger and smaller is kept by isOpen.
Start drawing
Let’s do some polygon shapes, and return a path.
func smallOpening() -> UIBezierPath { let bezierPath = UIBezierPath() bezierPath.move(to: CGPoint(x: 35.5, y: 49.5)) bezierPath.addLine(to: CGPoint(x: 41.5, y: 42.5)) bezierPath.addLine(to: CGPoint(x: 47.5, y: 42.5)) bezierPath.addLine(to: CGPoint(x: 47.5, y: 42.5)) bezierPath.addLine(to: CGPoint(x: 59.5, y: 49.5)) bezierPath.addLine(to: CGPoint(x: 66.5, y: 57.5)) bezierPath.addLine(to: CGPoint(x: 59.5, y: 57.5)) bezierPath.addLine(to: CGPoint(x: 47.5, y: 63.5)) bezierPath.addLine(to: CGPoint(x: 35.5, y: 57.5)) bezierPath.addLine(to: CGPoint(x: 35.5, y: 49.5)) bezierPath.addLine(to: CGPoint(x: 35.5, y: 49.5)) bezierPath.close() UIColor.gray.setFill() bezierPath.fill() UIColor.black.setStroke() bezierPath.lineWidth = 1 bezierPath.stroke() return bezierPath } func largeOpening() -> UIBezierPath { let bezierPath = UIBezierPath() bezierPath.move(to: CGPoint(x: 8.5, y: 82.5)) bezierPath.addLine(to: CGPoint(x: 15.5, y: 51.5)) bezierPath.addLine(to: CGPoint(x: 32.5, y: 21.5)) bezierPath.addLine(to: CGPoint(x: 51.5, y: 11.5)) bezierPath.addLine(to: CGPoint(x: 69.5, y: 21.5)) bezierPath.addLine(to: CGPoint(x: 82.5, y: 41.5)) bezierPath.addLine(to: CGPoint(x: 82.5, y: 64.5)) bezierPath.addLine(to: CGPoint(x: 87.5, y: 82.5)) bezierPath.addLine(to: CGPoint(x: 51.5, y: 91.5)) bezierPath.addLine(to: CGPoint(x: 8.5, y: 82.5)) bezierPath.addLine(to: CGPoint(x: 8.5, y: 82.5)) bezierPath.addLine(to: CGPoint(x: 8.5, y: 82.5)) bezierPath.close() UIColor.gray.setFill() bezierPath.fill() UIColor.black.setStroke() bezierPath.lineWidth = 1 bezierPath.stroke() return bezierPath }
Start animating
Now that we have shapes, let’s animate them from one to the other using CABasicAnimation, and changing the whole path.
func openShape() { let contractionAnimation = CABasicAnimation(keyPath: "path" ) contractionAnimation.fromValue = shape1?.cgPath contractionAnimation.toValue = shape2?.cgPath contractionAnimation.duration = 5.0 contractionAnimation.fillMode = kCAFillModeForwards contractionAnimation.isRemovedOnCompletion = false shapeLayer.add(contractionAnimation, forKey: "path" ) isOpen = true } func closeShape() { let contractionAnimation = CABasicAnimation(keyPath: "path" ) contractionAnimation.fromValue = shape2?.cgPath contractionAnimation.toValue = shape1?.cgPath contractionAnimation.duration = 5.0 contractionAnimation.fillMode = kCAFillModeForwards contractionAnimation.isRemovedOnCompletion = false shapeLayer.add(contractionAnimation, forKey: "path" ) isOpen = false }
We are animating from one path to the other, using the path key. We can change our path using fromValue and toValue. Set a duration. We want the animation to stay in place when it’s done, which is our isRemovedOnCompletion property. Set the state, so we know if we are open (bigger) or closed (smaller).
Load them up
Here we are creating our paths. A CAShapeLayer is all about the path, so we assign the smaller path to the it. Then we add the CAShapeLayer to the view layer (which is a CALayer).
override func viewDidLoad() { super.viewDidLoad() shape1 = smallOpening() shape2 = largeOpening() if let smallShape = shape1 { shapeLayer.path = smallShape.cgPath shapeLayer.fillColor = UIColor.gray.cgColor shapeLayer.strokeColor = UIColor.black.cgColor shapeView.layer.addSublayer(shapeLayer) } }
Connect the button
Standard iOS stuff, let’s toggle the animations based on our isOpen state.
@IBAction func toggle(_ sender: Any) { if isOpen { closeShape() } else { openShape() } }
Conclusion
This is a relatively small amount of code for this effect. Keep in mind that this is applied to a view. If you want to have views layered on top of one another to respect the shape edges, you’ll need to make the background color clear. Adding the sublayer will show your shape without the square of the view. Again, you can download the demo project.