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?
Editing your shapeIn your Timeline, select the Shape Layer and Drop Down the Layer Menu. Open the Contents dropdown and select the Shape you wish to Edit. Adjust the Color, Stroke, and Stroke Color in the Top Menu Bar. Edit the order of the Shapes (from back to front) by changing the Shape Stack in the Layer Panel.
A UIBezierPath object combines the geometry of a path with attributes that describe the path during rendering. You set the geometry and attributes separately and can change them independent of one another. After you have the object configured the way you want it, you can tell it to draw itself in the current context.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With