How do I animate constraint changes?

后端 未结 12 1681
野的像风
野的像风 2020-11-21 23:22

I\'m updating an old app with an AdBannerView and when there is no ad, it slides off screen. When there is an ad it slides on the screen. Basic stuff.

O

12条回答
  •  情话喂你
    2020-11-21 23:42

    Storyboard, Code, Tips and a few Gotchas

    The other answers are just fine but this one highlights a few fairly important gotchas of animating constraints using a recent example. I went through a lot of variations before I realized the following:

    Make the constraints you want to target into Class variables to hold a strong reference. In Swift I used lazy variables:

    lazy var centerYInflection:NSLayoutConstraint = {
           let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
            return temp!
    }()
    

    After some experimentation I noted that one MUST obtain the constraint from the view ABOVE (aka the superview) the two views where the constraint is defined. In the example below (both MNGStarRating and UIWebView are the two types of items I am creating a constraint between, and they are subviews within self.view).

    Filter Chaining

    I take advantage of Swift's filter method to separate the desired constraint that will serve as the inflection point. One could also get much more complicated but filter does a nice job here.

    Animating Constraints Using Swift

    Nota Bene - This example is the storyboard/code solution and assumes one has made default constraints in the storyboard. One can then animate the changes using code.

    Assuming you create a property to filter with accurate criteria and get to a specific inflection point for your animation (of course you could also filter for an array and loop through if you need multiple constraints):

    lazy var centerYInflection:NSLayoutConstraint = {
        let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
        return temp!
    }()
    

    ....

    Sometime later...

    @IBAction func toggleRatingView (sender:AnyObject){
    
        let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
    
        self.view.layoutIfNeeded()
    
    
        //Use any animation you want, I like the bounce in springVelocity...
        UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in
    
            //I use the frames to determine if the view is on-screen
            if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
    
                //in frame ~ animate away
                //I play a sound to give the animation some life
    
                self.centerYInflection.constant = aPointAboveScene
                self.centerYInflection.priority = UILayoutPriority(950)
    
            } else {
    
                //I play a different sound just to keep the user engaged
                //out of frame ~ animate into scene
                self.centerYInflection.constant = 0
                self.centerYInflection.priority = UILayoutPriority(950)
                self.view.setNeedsLayout()
                self.view.layoutIfNeeded()
             }) { (success) -> Void in
    
                //do something else
    
            }
        }
    }
    

    The many wrong turns

    These notes are really a set of tips that I wrote for myself. I did all the don'ts personally and painfully. Hopefully this guide can spare others.

    1. Watch out for zPositioning. Sometimes when nothing is apparently happening, you should hide some of the other views or use the view debugger to locate your animated view. I've even found cases where a User Defined Runtime Attribute was lost in a storyboard's xml and led to the animated view being covered (while working).

    2. Always take a minute to read the documentation (new and old), Quick Help, and headers. Apple keeps making a lot of changes to better manage AutoLayout constraints (see stack views). Or at least the AutoLayout Cookbook. Keep in mind that sometimes the best solutions are in the older documentation/videos.

    3. Play around with the values in the animation and consider using other animateWithDuration variants.

    4. Don't hardcode specific layout values as criteria for determining changes to other constants, instead use values that allow you to determine the location of the view. CGRectContainsRect is one example

    5. If needed, don't hesitate to use the layout margins associated with a view participating in the constraint definition let viewMargins = self.webview.layoutMarginsGuide: is on example
    6. Don't do work you don't have to do, all views with constraints on the storyboard have constraints attached to the property self.viewName.constraints
    7. Change your priorities for any constraints to less than 1000. I set mine to 250 (low) or 750 (high) on the storyboard; (if you try to change a 1000 priority to anything in code then the app will crash because 1000 is required)
    8. Consider not immediately trying to use activateConstraints and deactivateConstraints (they have their place but when just learning or if you are using a storyboard using these probably means your doing too much ~ they do have a place though as seen below)
    9. Consider not using addConstraints / removeConstraints unless you are really adding a new constraint in code. I found that most times I layout the views in the storyboard with desired constraints (placing the view offscreen), then in code, I animate the constraints previously created in the storyboard to move the view around.
    10. I spent a lot of wasted time building up constraints with the new NSAnchorLayout class and subclasses. These work just fine but it took me a while to realize that all the constraints that I needed already existed in the storyboard. If you build constraints in code then most certainly use this method to aggregate your constraints:

    Quick Sample Of Solutions to AVOID when using Storyboards

    private var _nc:[NSLayoutConstraint] = []
        lazy var newConstraints:[NSLayoutConstraint] = {
    
            if !(self._nc.isEmpty) {
                return self._nc
            }
    
            let viewMargins = self.webview.layoutMarginsGuide
            let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)
    
            let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
            centerY.constant = -1000.0
            centerY.priority = (950)
            let centerX =  self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
            centerX.priority = (950)
    
            if let buttonConstraints = self.originalRatingViewConstraints?.filter({
    
                ($0.firstItem is UIButton || $0.secondItem is UIButton )
            }) {
                self._nc.appendContentsOf(buttonConstraints)
    
            }
    
            self._nc.append( centerY)
            self._nc.append( centerX)
    
            self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
            self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
            self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
            self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))
    
            return self._nc
        }()
    

    If you forget one of these tips or the more simple ones such as where to add the layoutIfNeeded, most likely nothing will happen: In which case you may have a half baked solution like this:

    NB - Take a moment to read the AutoLayout Section Below and the original guide. There is a way to use these techniques to supplement your Dynamic Animators.

    UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in
    
                //
                if self.starTopInflectionPoint.constant < 0  {
                    //-3000
                    //offscreen
                    self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
                    self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
    
                } else {
    
                    self.starTopInflectionPoint.constant = -3000
                     self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
                }
    
            }) { (success) -> Void in
    
                //do something else
            }
    
        }
    

    Snippet from the AutoLayout Guide (note the second snippet is for using OS X). BTW - This is no longer in the current guide as far as I can see. The preferred techniques continue to evolve.

    Animating Changes Made by Auto Layout

    If you need full control over animating changes made by Auto Layout, you must make your constraint changes programmatically. The basic concept is the same for both iOS and OS X, but there are a few minor differences.

    In an iOS app, your code would look something like the following:

    [containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
    [UIView animateWithDuration:1.0 animations:^{
         // Make all constraint changes here
         [containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
    }];
    

    In OS X, use the following code when using layer-backed animations:

    [containterView layoutSubtreeIfNeeded];
    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
         [context setAllowsImplicitAnimation: YES];
         // Make all constraint changes here
         [containerView layoutSubtreeIfNeeded];
    }];
    

    When you aren’t using layer-backed animations, you must animate the constant using the constraint’s animator:

    [[constraint animator] setConstant:42];
    

    For those who learn better visually check out this early video from Apple.

    Pay Close Attention

    Often in documentation there are small notes or pieces of code that lead to bigger ideas. For example attaching auto layout constraints to dynamic animators is a big idea.

    Good Luck and May the Force be with you.

提交回复
热议问题