All the examples I could find online for UIViewPropertyAnimator
use views that are laid out by setting frame rather than using auto layout, which is how views are commonly laid out. When using auto layout, the view animates to the position I want, but afterwards I am not sure of how to set it back to its "model" position.
// Move a view that is constrained to the center of its superview
UIViewPropertyAnimator(duration: 1, curve: .linear) {
testView.center = CGPoint(x: 10, y: 10)
}.startAnimation()
It apparently adds some kind of constraint? I logged the container view's constraints before and after performing the animation:
CONSTRAINTS BEFORE:
view: [<NSLayoutConstraint:0x618000094f00 UIView:0x7fd764004050.centerX == UIView:0x7fd75fd00000.centerX (active)>, <NSLayoutConstraint:0x618000092de0 UIView:0x7fd764004050.centerY == UIView:0x7fd75fd00000.centerY (active)>]
CONSTRAINTS AFTER:
view: [<NSLayoutConstraint:0x618000094f00 UIView:0x7fd764004050.centerX == UIView:0x7fd75fd00000.centerX (active)>, <NSLayoutConstraint:0x618000092de0 UIView:0x7fd764004050.centerY == UIView:0x7fd75fd00000.centerY (active)>, <NSLayoutConstraint:0x6100000922a0 'UIView-Encapsulated-Layout-Height' UIView:0x7fd75fd00000.height == 667 (active)>, <NSAutoresizingMaskLayoutConstraint:0x610000092570 h=-&- v=-&- 'UIView-Encapsulated-Layout-Left' UIView:0x7fd75fd00000.minX == 0 (active, names: '|':UIWindow:0x7fd75fc07110 )>, <NSAutoresizingMaskLayoutConstraint:0x610000092890 h=-&- v=-&- 'UIView-Encapsulated-Layout-Top' UIView:0x7fd75fd00000.minY == 0 (active, names: '|':UIWindow:0x7fd75fc07110 )>, <NSLayoutConstraint:0x610000091670 'UIView-Encapsulated-Layout-Width' UIView:0x7fd75fd00000.width == 375 (active)>]
What's going on here? How should I handle the animation of views that are positioned with auto layout? Is Apple encouraging users to go back to frame-based layouts?
You can use UIViewPropertyAnimator
with Auto Layout, but you need to modify the constraints instead of the view's frame and call layoutIfNeeded()
inside of the closure.
First example: Animate by modifying constraint constant
Create a view in the Storyboard. Create a constraint to position the centerX
of your view equal to the leading edge of the superview. Create an @IBOutlet
to that constraint and call it centerXConstraint
. Similarly, create a centerYConstraint
that positions the centerY
of your view equal to the top of its superview. Both of these constraints should have constant = 0
.
Create @IBOutlet
s to the constraints:
@IBOutlet weak var centerXConstraint: NSLayoutConstraint!
@IBOutlet weak var centerYConstraint: NSLayoutConstraint!
Set the constant
values of the constraint to the new position and then call view.layoutIfNeeded()
inside of UIViewPropertyAnimator
:
// Make sure view has been laid out
view.layoutIfNeeded()
// Set new X and Y locations for the view
centerXConstraint.constant = 50
centerYConstraint.constant = 80
UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
self.view.layoutIfNeeded()
}.startAnimation()
Second example: Animate by activating new constraints
Create a view in your Storyboard (for example redView
) and create an @IBOutlet
to it in your code:
@IBOutlet weak var redView: UIView!
Create @IBOutlet
s to the constraints that control the position of your view
// Outlets to the constraints set in the Storyboard
@IBOutlet weak var topConstraint: NSLayoutConstraint!
@IBOutlet weak var leadingConstraint: NSLayoutConstraint!
Then when it's time to animate:
// Make sure view has been laid out
view.layoutIfNeeded()
// Deactivate the old constraints
topConstraint.isActive = false
leadingConstraint.isActive = false
// Active new constraints that move view to the bottom right
redView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
redView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// Animate the change
UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
self.view.layoutIfNeeded()
}.startAnimation()
So how is this better than the old UIView
animation blocks?
UIViewPropertyAnimator
is an object that you can configure to control the animation in various ways. Here is a complete example that demonstrates:
- Starting an animation
- Pausing an animation
- Finding out the fraction of the animation that has completed and setting the slide value to that
- Scrubbing through the animation with the slider
class ViewController: UIViewController {
@IBOutlet weak var redView: UIView!
@IBOutlet weak var centerXConstraint: NSLayoutConstraint!
@IBOutlet weak var centerYConstraint: NSLayoutConstraint!
@IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
slider.isHidden = true
}
var myAnimator: UIViewPropertyAnimator?
@IBAction func startAnimation(_ sender: UIButton) {
view.layoutIfNeeded()
centerXConstraint.isActive = false
centerYConstraint.isActive = false
redView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
redView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
myAnimator = UIViewPropertyAnimator(duration: 10, curve: .easeInOut) {
self.view.layoutIfNeeded()
}
myAnimator?.startAnimation()
}
@IBAction func pauseAnimation(_ sender: UIButton) {
guard let myAnimator = myAnimator else { return }
myAnimator.pauseAnimation()
print("The animation is \(String(format: "%.1f", myAnimator.fractionComplete * 100))% complete")
slider.value = Float(myAnimator.fractionComplete)
slider.isHidden = false
}
@IBAction func scrub(_ sender: UISlider) {
myAnimator?.fractionComplete = CGFloat(sender.value)
}
}