Question Comment animer les changements de contraintes?


Je suis en train de mettre à jour une ancienne application avec un AdBannerView et quand il n'y a pas d'annonce, elle glisse hors de l'écran. Quand il y a une publicité, elle glisse sur l'écran. Trucs de base.

Ancien style, j'ai mis 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 vue, 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.

METTRE À JOUR: J'ai regardé de nouveau la vidéo WWDC12 "Meilleures pratiques pour maîtriser la disposition 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 obtiens exactement 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 de côté, j'ai vérifié plusieurs fois et cela est en cours d'exécution sur le thread principal.


832
2017-09-27 13:23


origine


Réponses:


Deux notes importantes:

  1. Vous devez appeler layoutIfNeeded dans le bloc d'animation. Apple vous recommande de l'appeler une fois avant le bloc d'animation pour vous 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 parentale (par exemple. self.view), pas la vue enfant qui a les contraintes attachées à celle-ci. Cela mettra à jour tout les vues contraints, y compris l'animation d'autres vues qui pourraient être contraints à l'affichage de la contrainte de (par exemple, la vue B est attachée en bas de la vue A et vous venez de modifier le décalage supérieur de View A et vous voulez que B l'anime)

Essaye ça:

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()
}

1505
2017-09-30 19:00



J'apprécie la réponse fournie, mais je pense que ce serait bien d'aller un peu plus loin.

L'animation de base du bloc à partir 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 c'est vraiment un scénario très simpliste. Que faire si je veux animer des contraintes de sous-vue via le updateConstraints méthode?

Un bloc d'animation qui appelle la méthode sous-vues 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];

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

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

Le Guide AutoLayout laisse beaucoup à désirer mais cela vaut la peine d'être lu. Je l'utilise moi-même dans le cadre d'un UISwitch qui bascule une sous-vue avec une paire de UITextFields avec une animation d'effondrement simple et subtile (0,2 secondes de long). Les contraintes pour la sous-vue sont traitées dans les sous-classes UIView méthodes updateConstraints comme décrit ci-dessus.


96
2018-01-22 00:35



Généralement, il suffit de mettre à jour les contraintes et d'appeler layoutIfNeeded à l'intérieur du bloc d'animation. Cela peut soit changer le .constant propriété d'un NSLayoutConstraint, en ajoutant des contraintes de suppression (iOS 7) ou en modifiant .active propriété des contraintes (iOS 8 & 9).

Exemple de code:

[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

Il y a des questions à savoir si la contrainte doit être modifiée avant le bloc d'animation, ou à l'intérieur il (voir les réponses précédentes).

Ce qui suit est une conversation Twitter entre Martin Pilkington qui enseigne iOS, et Ken Ferry qui a écrit Auto Layout. Ken explique que, en changeant les constantes en dehors du 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ée. Il utilise Objective C et anime la vue en changeant le .active propriété de plusieurs contraintes. https://github.com/shepting/SampleAutoLayoutAnimation


58
2017-11-13 00:33



// 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
2017-12-23 14:29



Storyboard, code, conseils et quelques Gotchas

Les autres réponses sont très bien mais celle-ci met en évidence quelques pièges assez importants de l'animation des contraintes en utilisant un exemple récent. J'ai traversé beaucoup de variations avant de réaliser ce qui suit:

Faites en sorte que les contraintes que vous voulez cibler dans les variables de classe contiennent une référence forte. Dans Swift j'ai utilisé des variables paresseuses:

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!
}()

Après quelques expérimentations j'ai noté que l'on DOIT obtenir la contrainte de la vue AU DESSUS (alias the superview) les deux vues où la contrainte est définie. Dans l'exemple ci-dessous (à la fois MNGStarRating et UIWebView sont les deux types d'éléments que je crée une contrainte entre, et ils sont des sous-vues dans self.view).

Chaînage de filtres

Je profite de la méthode de filtrage de Swift pour séparer la contrainte désirée qui servira de point d'inflexion. On pourrait aussi devenir beaucoup plus compliqué mais le filtre fait du bon boulot ici.

Animation de contraintes à l'aide de Swift

Nota Bene - Cet exemple est la solution storyboard / code et suppose   on a fait des contraintes par défaut dans le storyboard. On peut alors   animer les changements en utilisant le code.

En supposant que vous créez une propriété à filtrer avec des critères précis et que vous atteignez un point d'inflexion spécifique pour votre animation (bien sûr, vous pouvez aussi filtrer pour un tableau et faire une boucle si vous avez besoin de plusieurs contraintes):

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!
}()

....

Un peu 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

        }
    }
}

Les nombreux mauvais tours

Ces notes sont vraiment un ensemble de conseils que j'ai écrits pour moi-même. J'ai fait tout ce que je n'ai pas fait personnellement et douloureusement. J'espère que ce guide peut épargner les autres.

  1. Attention à zPositioning. Parfois, quand rien n'est apparemment passe, vous devez masquer 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 Runtime défini par l'utilisateur Attribut a été perdu dans le fichier XML d'un storyboard et conduit à l'animé vue étant couverte (en travaillant).

  2. Prenez toujours une minute pour lire la documentation (nouvelle et ancienne), Quick Aide et en-têtes Apple continue de faire beaucoup de changements pour mieux gérer les contraintes AutoLayout (voir les vues de la pile). Ou au moins le Livre de recettes AutoLayout. Gardez à l'esprit que parfois les meilleures solutions sont dans l'ancienne documentation / vidéos.

  3. Jouez avec les valeurs de l'animation et envisagez d'utiliser autres variantes animateWithDuration.

  4. Ne pas coder en dur des valeurs de mise en page spécifiques 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. CGRectContainsRectest une Exemple

  5. Si nécessaire, n'hésitez pas à utiliser les marges de mise en page associées à une vue participant à la définition de contrainte let viewMargins = self.webview.layoutMarginsGuide: est sur l'exemple
  6. Ne faites pas le travail que vous n'avez pas à faire, toutes les vues avec des contraintes sur le storyboard ont des contraintes attachées à la propriété self.viewName.constraints
  7. Changez vos priorités pour toutes les contraintes à moins de 1000. Je définis mine à 250 (bas) ou 750 (haut) sur le storyboard; (Si vous essayez de changer une priorité de 1000 à quelque chose dans le code, l'application va planter, car 1000 est nécessaire)
  8. Ne pas essayer immédiatement d'utiliser activateConstraints et deactivateConstraints (ils ont leur place mais quand vous apprenez juste ou si vous utilisez un storyboard en utilisant ceux-ci signifie probablement que vous en faites trop ~ ils ont une place cependant comme vu ci-dessous)
  9. Envisagez de ne pas utiliser addConstraints / removeConstraints sauf si vous êtes vraiment ajouter une nouvelle contrainte dans le code. J'ai trouvé que la plupart du temps je disposition des vues dans le storyboard avec les contraintes souhaitées la vue hors écran), puis en code, j'anime les contraintes précédemment créées dans le storyboard pour déplacer la vue.
  10. J'ai passé beaucoup de temps perdu à accumuler des contraintes avec le nouveau NSAnchorLayout classe et sous-classes. Cela fonctionne très bien mais m'a pris un moment pour réaliser que toutes les contraintes dont j'avais besoin déjà existé dans le storyboard. Si vous générez des contraintes dans le code puis certainement utiliser cette méthode pour agréger vos contraintes:

Exemple rapide de solutions à éviter lorsque vous utilisez des 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
    }()

Si vous oubliez l'un de ces conseils ou les plus simples tels que l'endroit où ajouter la layoutIfNeeded, très probablement rien ne se passera: Dans ce cas, vous pouvez avoir une solution à moitié cuite comme ceci:

NB - Prenez un moment pour lire la section AutoLayout ci-dessous et la   guide original. Il y a un moyen 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 autant que je peux voir.  Les techniques préférées continuent d'évoluer.

Animation des modifications apportées par la disposition automatique

Si vous avez besoin d'un contrôle total sur l'animation des modifications apportées par la mise en page automatique, vous devez modifier vos contraintes par programmation. Le concept de base est le même pour iOS et OS X, mais il y a quelques différences mineures.

Dans une application iOS, votre code ressemblerait à 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
}];

Sous OS X, utilisez le code suivant lors de l'utilisation d'animations avec couches:

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

Lorsque vous n'utilisez pas d'animations avec couches, vous devez animer la constante à l'aide de l'animateur de contrainte:

[[constraint animator] setConstant:42];

Pour ceux qui apprennent mieux voir visuellement ce début vidéo d'Apple.

Porter une attention particulière

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

Bonne chance et que la force soit avec toi.


10
2017-12-11 01:49



Swift 4 solution

UIView.animate

Trois étapes simples:

  1. Changez les contraintes, par exemple:

    heightAnchor.constant = 50
    
  2. Dites le contenant view que sa mise en page est sale et que l'autolayout devrait recalculer la mise en page:

    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 directement les images (dans ce cas, l'autolayout définira les images):

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

Exemple le plus simple:

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

Sidenote

Il y a une 0ème étape optionnelle - avant de changer les contraintes que vous pourriez vouloir appeler self.view.layoutIfNeeded() pour s'assurer que le point de départ de l'animation provient de l'état avec les anciennes contraintes appliquées (au cas où il y aurait 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

Depuis iOS 10, nous avons un nouveau mécanisme d'animation - UIViewPropertyAnimator, nous devrions savoir que fondamentalement le même mécanisme s'applique à lui. Les étapes sont fondamentalement 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 garder une référence et l'appeler plus tard. Cependant, puisque dans le bloc d'animation nous disons simplement à l'autolayout de recalculer les images, 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 modification des contraintes et de démarrage d'un animateur est important - si nous changeons simplement les contraintes et laissons notre animateur à un point ultérieur, le cycle de rafraîchissement suivant peut invoquer le recalcul autolayout et le changement ne sera pas animé.

Souvenez-vous également qu'un seul animateur n'est pas réutilisable - une fois que vous l'avez exécuté, vous ne pouvez pas le "réexécuter". Donc, je suppose qu'il n'y a pas vraiment de bonne raison de garder l'animateur, sauf si on l'utilise pour contrôler une animation interactive.


7
2018-02-16 11:09



Solution rapide:

yourConstraint.constant = 50
UIView.animateWithDuration(1, animations: yourView.layoutIfNeeded)

6
2017-08-11 08:42



Solution de travail 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 ne fonctionnent pas, vous devriez les vérifier sur les appareils plus lents, par exemple l'iPhone 5.

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
2018-06-12 08:41



J'essayais d'animer Contraintes et n'était pas vraiment facile de trouver une bonne explication.

Les autres réponses sont totalement vraies: vous devez appeler [self.view layoutIfNeeded]; à l'intérieur animateWithDuration: animations:. Cependant, l'autre point important est d'avoir des pointeurs pour chaque NSLayoutConstraint vous voulez animer.

J'ai créé un exemple dans GitHub.


4
2017-11-25 08:24



Solution de travail et juste 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)

Gardez juste à l'esprit que self.calendarViewHeight est une contrainte référée à un customView (CalendarView). J'ai appelé .layoutIfNeeded () sur self.view et PAS sur self.calendarView

J'espère que cette aide.


3
2017-07-19 12:17



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 cela aide.


2
2017-09-13 08:35