Categories
iOS Swift UIKit

Interactive animation with UIViewPropertyAnimator on iOS

UIViewPropertyAnimator enables configuring animations which can be modified when running. Animations can be paused and progress can be changed allowing to build interactive animations. UIViewPropertyAnimations are in stopped state by default. If we want to run the animation immediately, we can use class method runningPropertyAnimator(withDuration:delay:options:animations:completion:). UIViewPropertyAnimator gives us a lot of flexibility when it comes to composing animations and controlling them. Therefore let’s build an animation consisting of rotating and moving a view out of the visible rect.

Adding a view to animate

Firstly, we need to add a view which we are going to animate using UIViewPropertyAnimator. View is a UIView subclass which just overrides layerClass and returns CAGradientLayer instead. View is positioned into initial place using auto layout.

private lazy var animatingView: UIView = {
let view = GradientView(frame: .zero)
view.gradientLayer.colors = (13).map({ "Gradient\($0)" }).map({ UIColor(named: $0)!.cgColor })
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
view.addGestureRecognizer(gestureRecognizer)
view.addSubview(animatingView)
NSLayoutConstraint.activate([
animatingView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 72),
animatingView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -72),
animatingView.topAnchor.constraint(equalTo: view.topAnchor, constant: 72),
animatingView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -72)
])
}

Creating animations with UIViewPropertyAnimator

UIViewPropertyAnimator has several initialisers allowing to control the used timing function. In this example we’ll just use built-in ease in and ease out timing. This just means animation pace increases in the beginning and slows down at the end of the animation. In addition to mentioned UICubicTimingParameters (ease in and ease out), there is support for UISpringTimingParameters as well. Both timing parameters can be passed in using the convenience initialiser init(duration:timingParameters:). The animation is configured to rotate the view by 90 degrees and move the view following a spline created by current point of the view and two other points. When animation ends, we reset the transform and tell auto layout to update the layout which will just move the view back to the initial position.

private func makeAnimator() -> UIViewPropertyAnimator {
let animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut)
let bounds = view.bounds
animator.addAnimations { [weak animatingView] in
guard let animatingView = animatingView else { return }
animatingView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4, animations: {
animatingView.center = CGPoint(x: bounds.width * 0.8, y: bounds.height * 0.85)
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.6, animations: {
animatingView.center = CGPoint(x: bounds.width + animatingView.bounds.height, y: bounds.height * 0.6)
})
})
}
animator.addCompletion({ [weak self] (_) in
guard let self = self else { return }
self.animatingView.transform = CGAffineTransform(rotationAngle: 0.0)
self.animator = nil
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
})
return animator
}
view raw Animator.swift hosted with ❤ by GitHub

Interrupting animation with UIPanGestureRecognizer

UIPanGestureRecognizer is used for interrupting the animation. When user starts dragging a finger on the screen, we capture the current animation progress and the initial point of the touch. Then, we can update the animation progress when dragging the finger to the left or right. When moving the finger back and forth, we can move the animation forward or backwards. As soon as letting the finger go, we start the animation which continues the animation from the update fractionComplete. The constant 300 is just a value defining the amount user needs to move the finger to be able to change the fractionComplete from 0.0 to 1.0.

private lazy var gestureRecognizer: UIPanGestureRecognizer = {
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(updateProgress(_:)))
recognizer.maximumNumberOfTouches = 1
return recognizer
}()
@objc private func updateProgress(_ recognizer: UIPanGestureRecognizer) {
if animator == nil {
animator = makeAnimator()
}
guard let animator = animator else { return }
switch recognizer.state {
case .began:
animator.pauseAnimation()
fractionCompletedStart = animator.fractionComplete
dragStartPosition = recognizer.location(in: view)
case .changed:
animator.pauseAnimation()
let delta = recognizer.location(in: view).x dragStartPosition.x
animator.fractionComplete = max(0.0, min(1.0, fractionCompletedStart + delta / 300.0))
case .ended:
animator.startAnimation()
default:
break
}
}

Summary

With UIViewPropertyAnimator we can build interactive animations with a very little code. Its API allows controlling the flow of the animations by pausing the animation and controlling the progress of the animation dynamically.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

InteractiveAnimation (Xcode 10.2, Swift 5)

Resources

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s