Gesture to Animation
When the user flicks a view, we don’t want to start a standard animation with a standard speed, instead we want to use the gesture speed:
UIPanGestureRecognizerusefunc velocityInView(view: UIView) -> CGPoint(pan velocity)UIPinchGestureRecognizerusevar velocity: CGFloat { get }(velocity of the scale)UIRotationGestureRecognizerusevar velocity: CGFloat { get }(angular velocity for that rotation)
How do we start our animation at that velocity?
Option 1: UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)
From iOS 7 we have UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:), where we pass an initialSpringVelocity.
This velocity is a normalized velocity in a normalized coordinate space, we want to normalize it based on the total distance that our view is going to travel during the animation:
first, we have to calculate the distance (from where we start to where we want to go) in points (e.g., 100 pt)
then, we take our initial velocity (from the gesture recognizer) in points/sec (e.g., 50 pt/sec)
lastly we normalize the view by dividing our initial velocity by the distance (e.g., 50 pt/sec / 100 pt = 0.5 units/sec)
we can now pass this normalized value in
initialSpringVelocity
Option 2: UIDynamicAnimator
Setup:
var dynamicAnimator: UIDynamicAnimator?
let dynamicItemBehavior = UIDynamicItemBehavior(items: nil)
override func viewDidLoad() {
super.viewDidLoad()
dynamicAnimator = UIDynamicAnimator(referenceView: view)
dynamicItemBehavior.resistance = 3.0
dynamicItemBehavior.angularResistance = 3.0
dynamicAnimator!.addBehavior(dynamicItemBehavior)
}Transfer the velocity:
// in the gesture handle
let targetView = panGestureRecognizer.view
switch panGestureRecognizer.state {
case .ended:
let v = panGestureRecognizer.velocityInView(targetView.superview)
dynamicItemBehavior.addLinearVelocity(v, forItem: targetView)
case ...:
}Option 3: CADisplayLink
CADisplayLink calls you back once every frame:
when frame is going to get rendered, you get called back to go update your app in whatever way you want
this is how
UIDynamicAnimatordoes itenables you to go full custom on the animation
Setup:
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: .defaultRunLoopMode)
}
func step(displaylink: CADisplayLink) {
// do your drawing here.
}Pros and Cons.
UIView.animatepushes work to the render server, hence the application is free to do other work.CADisplayLinkis done in the main thread.UIDynamicAnimatorcan create more advanced interactions (e.g., collision within views)CADisplayLinklets you completely control what to draw
Animation to Animation
From iOS 8 all UIView animations will be additive by default.
This means that when we write:
UIView.animateWithDuration(1) {
// animations here
}Behind the scenes, a new CAAnimation instance will be created that will have its isAdditive property set to true.
Before iOS 8, UIView.animateWithDuration would remove the current CAAnimation and add a new one without any regard of where in the animation we were. This was causing our view to jump unexpectedly.
From iOS 8, calling UIView.animateWithDuration will add a new CAAnimation, and the old CAAnimation will continue to exist until they complete before being removed. This accomplishes a much more fluid experience.
Note that this is different than using UIView.animate with .beginFromCurrentState, as that will completely stop/remove the current CAAnimation, and add a new CAAnimation that starts from whichever state the presentation model was at that instant (this makes the view go full stop, and start a new animation from scratch, possibly towards a different direction, this is better than jumping, but still not fluid).
Supported keys for additive animations:
centerframeboundstransformlayer.transform// only for affine transformations, where the layer parallel lines/edges are still parallel (e.g. for y rotation, but not z rotation)
Compatibility requirements for additive animations:
No keyframe animations
No pre-existing repeating animations
No pre-existing absolute animations
From iOS 8, when using UIView.animatewith .beginFromCurrentState, iOS will still try to use additive animations:
UIView.animateWithDuration(1,
delay: 0,
options: .beginFromCurrentState,
animations: {
circle.center.x = finalValue // supported, will use additive animation
circle.alpha = 0 // not supported, will use beginFromCurrentState CAAnimation
circle.tintColor = UIColor.redColor // not supported, will use beginFromCurrentState CAAnimation
},
completion: nil)How to cancel animations
The old way of using an animation with zero duration no longer works, as it now just adds a new additive animation:
UIView.animateWithDuration(0) {
circle.center.x = finalValue // this no longer works from iOS 8
}Instead, we now need to go to the layer and remove the animations ourselves:
view.layer.removeAnimation(animation)Animation completion
With additive animations, now all completion blocks will be called when all the concurrent animations will end.
We no longer get the completion block called with false when a new additive animation is added on top of our animation, instead, our completion is called when both animation end and the isFinished parameter will be true.
Animation to Gesture
When we trigger an animation via UIView.animate, our animated views can catch gestures, but they are ignored by default.
To change this, we can pass .allowUserInteraction as one of the animation options:
when we pass this option, our view no longer catches gesture during the animation, as the view position is considered as if it’s at its destination already. This is because iOS doesn’t know if the thing that you’re animating is something that you intend to interact with or just something that is animating in a system, but you’re actually trying to interact with the thing behind it.
In other words, with .allowUserInteraction we’re doing a model value hit test instead of a presentation value hit test.
If we want to catch gestures in the view we’re animating (with .allowUserInteraction in the animation), we need to override the view hitTest and make a presentation layer hit test instead of a model layer hit test:
override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView! {
let superviewPoint = convertPoint(point, toView: superview)
let point = layer.presentationLayer.convertPoint(superviewPoint, fromLayer: superview.layer)
return super.hitTest(point, withEvent: event)
}Note that once we do this, all the rest of UIKit APIs will still interact with the model layer, hence when we call things such as touch.locationInView, they will be returned accordingly to the model layer.
Stop Animating
To stop the animation, we need to:
get the current value from the presentation layer
set said value to our model layer
remove the animation
// example of stopping a transition animation
let presentationPosition = view.layer.presentationLayer().position
// note that CALayer.position is equivalent to UIView.center
view.center = presentationPosition
view.layer.removeAllAnimations()If we use UIDynamicAnimator things are simpler:
because
UIDynamicAnimatordoesn’t have presentation and model space, we don’t have this same complication of having to figure out where it is on-screen compared to where the model is or anything like thatUIDynamicAnimatordata is always up to date to wherever is happening in your process, and the model value is the correct position on screen right now
A way to stop animation is by removing the behavior from the view in our gesture handle:
switch (panGestureRecognizer.state) {
case .began:
dynamicItemBehavior.removeItem(targetView) // 👈🏻 remove behavior here
case .ended:
let v = panGestureRecognizer.velocityInView(targetView.superview)
dynamicItemBehavior.addItem(targetView) // 👈🏻 add it back here
dynamicItemBehavior.addLinearVelocity(v, forItem: targetView)
}