Animating Polygon Shape Changes With CAShapeLayer in Swift and iOS

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.

Category(s): iOS

Comments are closed.