Comment animer les changements de contraintes?

je mets à jour une vieille application avec un AdBannerView et quand il n'y a pas de publicité, elle glisse hors de l'écran. Quand il ya une annonce, il glisse sur l'écran. Des trucs de base.

Style Ancien, j'ai placé le cadre dans un bloc d'animation. Nouveau style, j'ai un IBOutlet à la contrainte qui détermine la position Y, dans ce cas c'est la distance du bas de la superview, et modifier la constante.

- (void)moveBannerOffScreen {
    [UIView animateWithDuration:5
             animations:^{
                          _addBannerDistanceFromBottomConstraint.constant = -32;
                     }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen {
    [UIView animateWithDuration:5
             animations:^{
                         _addBannerDistanceFromBottomConstraint.constant = 0;
             }];
    bannerIsVisible = TRUE;
}

Et la bannière se déplace, exactement comme prévu, mais pas d'animation.

mise à JOUR: j'ai re-regardé WWDC12 vidéo " Meilleures Pratiques pour la maîtrise de la Mise en page Automatique " qui couvre l'animation. Il explique comment mettre à jour les contraintes en utilisant CoreAnimation .

enter image description here enter image description here

j'ai essayé avec le code suivant, mais obtenir les mêmes résultats.

- (void)moveBannerOffScreen {
    _addBannerDistanceFromBottomConstraint.constant = -32;
    [UIView animateWithDuration:2
                     animations:^{
                         [self.view setNeedsLayout];
                     }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen {
    _addBannerDistanceFromBottomConstraint.constant = 0;
    [UIView animateWithDuration:2
                     animations:^{
                         [self.view setNeedsLayout];
                     }];
    bannerIsVisible = TRUE;
}

sur une note latérale, Je ont vérifié de nombreuses fois et ceci est exécuté sur le thread principal.

848
demandé sur hashier 2012-09-27 17:23:06

13 réponses

deux notes importantes:

  1. vous devez appeler layoutIfNeeded dans le bloc d'animation. Apple recommande en fait de l'appeler une fois avant le bloc d'animation pour s'assurer que toutes les opérations de mise en page en attente ont été effectuées

  2. vous devez l'appeler spécifiquement sur le vue de parent (par exemple self.view ), pas la vue de l'enfant qui a les contraintes attachées à il. Faire ainsi mettra à jour toutes vues limitées, y compris animer d'autres vues qui pourraient être limitées à la vue que vous avez changé la contrainte de (par exemple, la vue B est attachée au bas de la vue A et vous venez de changer l'offset supérieur de la vue A et vous voulez que la vue B animer avec elle)

Essayez ce qui suit:

Objectif-C

- (void)moveBannerOffScreen {
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = -32;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen { 
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = 0;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = TRUE;
}

Swift 3

UIView.animate(withDuration: 5) {
    self._addBannerDistanceFromBottomConstraint.constant = 0
    self.view.layoutIfNeeded()
}
1525
répondu g3rv4 2018-05-14 18:55:58

j'apprécie la réponse, mais je pense qu'il serait bien de prendre un peu plus loin.

l'animation de bloc de base de la documentation

[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
}];

, mais vraiment c'est un scénario très simpliste. Que faire si je veux animer les contraintes de subview via la méthode updateConstraints ?

un bloc d'animation qui appelle la méthode updateConstraints

[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
    [self.view layoutIfNeeded];
} completion:nil];

le la méthode updateConstraints est dépassée dans la sous-classe UIView et doit appeler super à la fin de la méthode.

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

La mise en page automatique Guide laisse beaucoup à désirer, mais il vaut la peine de lire. Je l'utilise moi-même dans le cadre d'un UISwitch qui fait basculer une subview avec une paire de UITextField s avec une animation d'effondrement simple et subtile (0.2 seconde). Les contraintes de subview sont traitées dans les sous-classes UIView les méthodes updateConstraints décrites ci-dessus.

98
répondu Cameron Lowell Palmer 2016-04-20 09:27:50

Généralement, il suffit de mettre à jour les contraintes et d'appeler layoutIfNeeded dans le bloc d'animation. Il peut s'agir de modifier la propriété .constant d'un NSLayoutConstraint , d'Ajouter Supprimer des contraintes (iOS 7), ou de modifier la propriété .active de contraintes (iOS 8 & 9).

Code Échantillon:

[UIView animateWithDuration:0.3 animations:^{
    // Move to right
    self.leadingConstraint.active = false;
    self.trailingConstraint.active = true;

    // Move to bottom
    self.topConstraint.active = false;
    self.bottomConstraint.active = true;

    // Make the animation happen
    [self.view setNeedsLayout];
    [self.view layoutIfNeeded];
}];

Exemple De Configuration:

Xcode Project so sample animation project.

Controverse

on se demande si la contrainte doit être changée avant le bloc d'animation, ou à l'intérieur it (voir les réponses précédentes).

ce qui suit est une conversation sur Twitter entre Martin Pilkington qui enseigne à iOS, et Ken Ferry qui a écrit Auto Layout. Ken explique que, bien que changeant les constantes en dehors de la bloc d'animation peut actuellement travailler, ce n'est pas sûr et ils devraient vraiment être changer à l'intérieur le bloc d'animation. https://twitter.com/kongtomorrow/status/440627401018466305

Animation:

Exemple De Projet

voici un projet simple montrant comment une vue peut être animé. Il utilise L'Objectif C et anime la vue en changeant la propriété .active de plusieurs contraintes. https://github.com/shepting/SampleAutoLayoutAnimation

60
répondu Steven Hepting 2015-11-13 00:52:08
// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)

// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{

    // Step 3, call layoutIfNeeded on your animated view's parent
    [self.view layoutIfNeeded];
}];
32
répondu John Erck 2013-12-23 14:29:37

Storyboard, du Code, des Conseils et quelques Astuces

les autres réponses sont très bien, mais celle-ci met en évidence quelques gotchas assez importantes de l'animation des contraintes à l'aide d'un exemple récent. Je suis passé par beaucoup de variations avant de réaliser ce qui suit:

faire les contraintes que vous voulez cibler en variables de classe pour tenir une référence forte. Dans Swift, j'ai utilisé des variables paresseuses:

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

après quelques expérimentation j'ai noté que l'on doit obtenir la contrainte de la vue au-dessus de (aka la superview) les deux vues où la contrainte est définie. Dans l'exemple ci-dessous (mngstarrating et UIWebView sont les deux types d'éléments que je crée une contrainte entre, et ils sont des sous-vues en soi.vue.)

Chaîne De Filtres

je profite de la méthode de filtrage de Swift pour séparer les souhaité contrainte qui servira de point d'inflexion. On pourrait aussi devenir beaucoup plus compliqué mais filter fait un bon travail ici.

Animation Des Contraintes De L'Utilisation De Swift

Nota Bene - Cet exemple est la table de montage séquentiel/solution de code et assume on a fait des contraintes par défaut dans le storyboard. On peut alors animer les modifications en utilisant le code.

en supposant que vous créer une propriété pour filtrer avec des critères précis et obtenir un point d'inflexion spécifique pour votre animation (bien sûr, vous pouvez également filtrer pour un tableau et boucle si vous avez besoin de multiples contraintes):

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

....

plus tard...

@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

        }
    }
}

Le nombre de mauvais tours

Ces notes sont vraiment un ensemble de conseils que j'ai écrit pour moi-même. J'ai fait tout le don'ts personnellement et douloureusement. Espérons que ce guide pourra en épargner d'autres.

  1. attention à la position Z. Parfois, quand rien n'est apparemment happening, vous devriez cacher certaines des autres vues ou utiliser la vue débogueur pour localiser votre vue animée. J'ai même trouvé des cas où un utilisateur a défini Runtime Attribut a été perdu dans le xml d'un storyboard et a conduit à l'animé vue d'être recouverts (tout en travaillant).

  2. toujours prendre une minute pour lire la documentation (nouvelle et ancienne), vite De l'aide, et les en-têtes. Apple continue de faire beaucoup de changements pour mieux Gérer les contraintes D'AutoLayout (voir vues stack). Ou au moins le Autolayout Cookbook . Gardez à l'esprit que parfois les meilleures solutions se trouvent dans les anciennes documentations/vidéos.

  3. jouer avec les valeurs de l'animation et envisager d'utiliser autres animauxavec des variantes de duration.

  4. Ne pas coder en dur la mise en page spécifique de valeurs comme critères pour déterminer changements à d'autres constantes, utilisez plutôt des valeurs qui vous permettent de déterminer l'emplacement de la vue. CGRectContainsRect est un exemple

  5. si nécessaire, n'hésitez pas à utiliser les marges de mise en page une vue de participer à la définition de la contrainte let viewMargins = self.webview.layoutMarginsGuide : est à l'exemple
  6. ne pas faire le travail, vous n'avez pas à le faire, tous les vues avec des contraintes sur les storyboard ont des contraintes attachées à la propriété auto.viewName.contraintes
  7. changez vos priorités pour les contraintes à moins de 1000. J'ai mis mine à 250 (bas) ou 750 (haut) sur le storyboard; (si vous essayez de changer une priorité de 1000 à quoi que ce soit dans le code, puis l'application se crash parce que 1000 est nécessaire)
  8. envisager de ne pas essayer d'utiliser immédiatement activateConstraints et ils disposent de leurs propres lieu mais quand juste apprendre ou si vous utilisez un storyboard en utilisant ceux-ci signifie probablement que vous faites trop ~ ils ont une place bien que comme vu ci-dessous)
  9. N'utilisez pas addConstraints / removeConstraints à moins que vous ne soyez vraiment ajouter une nouvelle contrainte dans le code. J'ai constaté que la plupart du temps je la disposition des vues dans le storyboard avec les contraintes souhaitées (placer la vue offscreen), puis en code, j'anime les contraintes précédemment créées dans le storyboard pour déplacer le point de vue autour de.
  10. j'ai passé beaucoup de temps perdu à construire des contraintes avec le nouveau NSAnchorLayout classe et sous-classes. Celles-ci fonctionnent très bien, mais il ça m'a pris du temps de réaliser que toutes les contraintes dont j'avais besoin il existait déjà dans le storyboard. Si vous construisez des contraintes en code puis très certainement utiliser cette méthode pour agréger vos contraintes:

échantillon rapide de Solutions à éviter lors de L'utilisation de 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({

            ("151930920".firstItem is UIButton || "151930920".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
    }()

si vous oubliez un de ces bouts ou les plus simples comme où ajouter le layoutifned, très probablement rien ne se passera: dans ce cas, vous pouvez avoir une solution à moitié cuit comme ceci:

NB - prendre un moment pour lire la Section AutoLayout ci-dessous et le guide original. Il y a une façon d'utiliser ces techniques pour compléter vos animateurs dynamiques.

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
        }

    }

extrait du Guide AutoLayout (notez que le second extrait est pour utiliser OS X). BTW - ce n'est plus dans le guide actuel aussi loin que je peux voir. les techniques préférées continuent d'évoluer.

Animation des Modifications Apportées par la Mise en page Automatique

si vous avez besoin d'un contrôle total sur l'animation des modifications faites par la mise en page automatique, vous devez faire vos modifications de contraintes programmatiquement. Base le concept est le même pour iOS et OS X, mais il y a quelques différences mineures.

dans une application iOS, votre code ressemblerait à quelque chose comme ceci:

[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
}];

dans OS X, utilisez le code suivant lors de l'utilisation des animations de calques:

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

quand vous n'utilisez pas les animations en calques, vous devez animer la constante en utilisant l'animateur de la contrainte:

[[constraint animator] setConstant:42];

pour ceux qui apprennent mieux visuellement vérifier ce début vidéo D'Apple .

Faites Très Attention

souvent dans la documentation il y a des petites notes ou des morceaux de code qui mènent à des idées plus grandes. Par exemple, attacher des contraintes de mise en page automatique aux animateurs dynamiques est une grande idée.

bonne chance et que la Force soit avec vous.

11
répondu Tommie C. 2017-05-02 00:44:32

Swift 4 solution

UIView.animer

trois étapes simples:

  1. Modifier les contraintes, p.ex.:

    heightAnchor.constant = 50
    
  2. dire au contenant view que sa disposition est sale et que l'autolayout devrait recalculer la disposition:

    self.view.setNeedsLayout()
    
  3. dans le bloc d'animation, dites à la mise en page de recalculer la mise en page, ce qui équivaut à définir les cadres directement (dans ce cas, l'autolayout définira les cadres):

    UIView.animate(withDuration: 0.5) {
        self.view.layoutIfNeeded()
    }
    

Complete simple example:

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

Note

il y a un 0ème pas optionnel-avant de changer les contraintes vous vous pouvez appeler self.view.layoutIfNeeded() pour vous assurer que le point de départ de l'animation est l'état avec les anciennes contraintes appliquées (dans le cas où il y avait d'autres changements de contraintes qui ne devraient pas être inclus dans l'animation):

otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

UIViewPropertyAnimator

Puisqu'avec iOS 10 nous avons un nouveau mécanisme d'animation - UIViewPropertyAnimator , nous devrions savoir que fondamentalement le même mécanisme s'applique à elle. Les étapes sont essentiellement les mêmes:

heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}
animator.startAnimation()

Depuis animator est une encapsulation de l'animation, nous pouvons faire référence à elle et l'appeler plus tard. Cependant, puisque dans le bloc d'animation nous disons simplement à l'autolayout de recalculer les frames, nous devons changer les contraintes avant d'appeler startAnimation . Par conséquent quelque chose comme ceci est possible:

// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}

// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()

L'ordre de l'évolution des contraintes et le démarrage d'une l'animateur est important - si nous changeons simplement les contraintes et laissons notre animateur pour un point ultérieur, le prochain cycle de redessination peut invoquer le recalcul autolayout et le changement ne sera pas animé.

Aussi, n'oubliez pas qu'un simple animateur est non-réutilisable - une fois que vous l'exécutez, vous ne pouvez pas "réexécuter". Donc je suppose qu'il n'y a pas vraiment de bonne raison de garder l'animateur autour, à moins que nous l'utilisions pour contrôler une animation interactive.

8
répondu Milan Nosáľ 2018-02-16 11:09:32

solution Swift:

yourConstraint.constant = 50
UIView.animateWithDuration(1, animations: yourView.layoutIfNeeded)
6
répondu Nazariy Vlizlo 2016-08-11 08:42:41

Working Solution 100% Swift 3.1

j'ai lu toutes les réponses et je veux partager le code et la hiérarchie des lignes que j'ai utilisées dans toutes mes applications pour les animer correctement, certaines solutions ici ne fonctionnent pas, vous devriez les vérifier sur les appareils plus lents E. g iPhone 5 en ce moment.

self.view.layoutIfNeeded() // Force lays of all subviews on root view
UIView.animate(withDuration: 0.5) { [weak self] in // allowing to ARC to deallocate it properly
       self?.tbConstraint.constant = 158 // my constraint constant change
       self?.view.layoutIfNeeded() // Force lays of all subviews on root view again.
}
5
répondu C0mrade 2017-06-12 08:41:17

j'essayais d'animer les contraintes et n'étais pas vraiment facile à trouver une bonne explication.

ce que d'autres réponses disent est totalement vrai: vous devez appeler [self.view layoutIfNeeded]; à l'intérieur de animateWithDuration: animations: . Cependant, l'autre point important est d'avoir des pointeurs pour tous NSLayoutConstraint que vous souhaitez animer.

j'ai créé un exemple dans github .

4
répondu Gabriel.Massana 2015-04-23 08:49:38

solution de travail et vient d'être testée pour Swift 3 avec Xcode 8.3.3:

self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0

UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
        self.view.layoutIfNeeded()
    }, completion: nil)

il suffit de garder à l'esprit ce soi.calendarViewHeight est une contrainte référencée à un customView (CalendarView). J'ai appelé le .layoutIfNeeded() sur l'auto.vue et PAS sur de soi.calendarView

Espérons que cette aide.

3
répondu Edoardo Vicoli 2017-07-19 12:17:43

il y a un article à ce sujet: http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was

dans lequel, il a codé comme ceci:

- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
    if (_isVisible) {
        _isVisible = NO;
        self.topConstraint.constant = -44.;    // 1
        [self.navbar setNeedsUpdateConstraints];  // 2
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded]; // 3
        }];
    } else {
        _isVisible = YES;
        self.topConstraint.constant = 0.;
        [self.navbar setNeedsUpdateConstraints];
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded];
        }];
    }
}

J'espère que ça aidera.

2
répondu D.D. 2013-09-13 08:35:13

dans le contexte de l'animation contrainte, je voudrais mentionner une situation spécifique où j'ai animé une contrainte immédiatement dans une notification keyboard_opened.

La contrainte

définit un espace supérieur allant d'un champ de texte au dessus du conteneur. En ouvrant le clavier, je divise la constante par 2.

Je n'ai pas été en mesure de réaliser une animation douce et cohérente des contraintes directement dans la notification clavier. Environ la moitié du temps d'affichage serait simplement sauter à sa nouvelle position-sans animer.

cela m'est venu à l'esprit qu'il pourrait y avoir des mises en scène supplémentaires à la suite de l'ouverture du clavier. L'ajout d'un bloc dispatch_after simple avec un délai de 10ms a fait courir l'animation à chaque fois - pas de saut.

1
répondu ivanferdelja 2016-04-15 15:54:27

pour les autres Xamariens, voici le Xamarin.iOS / C # version:

UIView.Animate(5, () =>
{
    _addBannerDistanceFromBottomConstraint.Constant = 0;
    View.LayoutIfNeeded();
});
0
répondu Martin Zikmund 2018-10-03 08:46:51