How to use UIViewPropertyAnimator with auto layout

2019-05-06 23:25发布

问题:

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?

回答1:

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 @IBOutlets 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 @IBOutlets 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:

  1. Starting an animation
  2. Pausing an animation
  3. Finding out the fraction of the animation that has completed and setting the slide value to that
  4. 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)
    }
}


回答2:

I made a tutorial for swift 4.1