Animating between two bezier path shapes

coco picture coco · Feb 10, 2015 · Viewed 9.8k times · Source

I found it tricky to animate a UIImageView between two states: its original rectangle frame, and a new shape created with a UIBezierPath. There are many different techniques mentioned, most of which did not work for me.

First was the realization that using UIView block animation would not work; evidently one can't perform sublayer animations in an animateWithDuration: block. (see here and here)

That left CAAnimation, with the concrete subclasses like CABasicAnimation. I soon realized that one can't animate from a view that doesn't have a CAShapeLayer to one that does (see here, for example).

And they can't be just any two shape layer paths, but rather "Animating the path of a shape layer is only guaranteed to work when you are animating from like to like" (see here)

With that in place, comes the more mundane problems, like what to use for fromValue and toValue (should they be a CAShapeLayer, or a CGPath?), what to add the animation to (the layer, or the mask?), etc.

It seemed there were so many variables; which combination would give me the animation I was looking for?

Answer

coco picture coco · Feb 10, 2015

The first important point is to construct the two bezier paths similarly, so the rectangle is a (trivial) analogue to the more complex shape.

// the complex bezier path
let initialPoint = CGPoint(x: 0, y: 0)
let curveStart = CGPoint(x: 0, y: (rect.size.height) * (0.2))
let curveControl = CGPoint(x: (rect.size.width) * (0.6), y: (rect.size.height) * (0.5))
let curveEnd = CGPoint(x: 0, y: (rect.size.height) * (0.8))
let firstCorner = CGPoint(x: 0, y: rect.size.height)
let secondCorner = CGPoint(x: rect.size.width, y: rect.size.height)
let thirdCorner = CGPoint(x: rect.size.width, y: 0)

var myBezierArc = UIBezierPath()
myBezierArc.moveToPoint(initialPoint)
myBezierArc.addLineToPoint(curveStart)
myBezierArc.addQuadCurveToPoint(curveEnd, controlPoint: curveControl)
myBezierArc.addLineToPoint(firstCorner)
myBezierArc.addLineToPoint(secondCorner)
myBezierArc.addLineToPoint(thirdCorner)

The simpler 'trivial' bezier path, that creates a rectangle, is exactly the same but the controlPoint is set so that it appears to not be there:

let curveControl = CGPoint(x: 0, y: (rect.size.height) * (0.5))

( Try removing the addQuadCurveToPoint line to get a very strange animation! )

And finally, the animation commands:

let myAnimation = CABasicAnimation(keyPath: "path")

if (isArcVisible == true) {
    myAnimation.fromValue = myBezierArc.CGPath
    myAnimation.toValue = myBezierTrivial.CGPath
} else {
    myAnimation.fromValue = myBezierTrivial.CGPath
    myAnimation.toValue = myBezierArc.CGPath
}       
myAnimation.duration = 0.4
myAnimation.fillMode = kCAFillModeForwards
myAnimation.removedOnCompletion = false

myImageView.layer.mask.addAnimation(myAnimation, forKey: "animatePath")

If anyone is interested, the project is here.