web-dev-qa-db-fra.com

Comment animer des modifications de contraintes?

Je mets à jour une ancienne application avec un AdBannerView et, lorsqu'il n'y a pas d'annonce, elle disparaît de l'écran. Quand il y a une annonce, celle-ci glisse à 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 de mise en page automatique qui détermine la position Y, dans ce cas c'est la distance par rapport au bas de la vue d'ensemble, et je modifie 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 no animation.

UPDATE: J'ai regardé de nouveau Meilleures pratiques pour parler de la WWDC 12 afin de maîtriser la mise en page automatique qui couvre l'animation. Il explique comment mettre à jour les contraintes en utilisant CoreAnimation :

J'ai essayé avec le code suivant, mais j'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é de nombreuses fois et cela est exécuté sur le thread main.

919
DBD

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 sont terminées.

  2. Vous devez l’appeler spécifiquement sur la vue vue parent (par exemple self.view), et non sur la vue enfant à laquelle les contraintes sont attachées. Cela mettra à jour toutes les vues contraintes, y compris l'animation d'autres vues susceptibles d'être contraintes à la vue pour laquelle vous avez modifié la contrainte (par exemple, la vue B est attachée). le bas de la vue A et vous venez de modifier le décalage supérieur de la vue A et vous souhaitez que la vue B s'anime avec elle)

Essaye ça:

Objective-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

UIView.animate(withDuration: 5) {
    self._addBannerDistanceFromBottomConstraint.constant = 0
    self.view.layoutIfNeeded()
}
1630
g3rv4

J'apprécie la réponse fournie, mais je pense qu'il serait bien d'aller 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 c'est vraiment un scénario très simpliste. Que faire si je souhaite animer des contraintes de sous-vue via la méthode updateConstraints?

Un bloc d'animation qui appelle la méthode updateConstraints subviews

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

The AutoLayout Guide laisse beaucoup à désirer mais cela vaut la peine d'être lu. J'utilise moi-même cela dans le cadre d'une UISwitch qui bascule une sous-vue avec une paire de UITextFields avec une animation de réduction simple et subtile (longue de 0,2 seconde). Les contraintes de la sous-vue sont gérées dans les méthodes updateConstraints des sous-classes UIView décrites ci-dessus.

106

Généralement, il vous suffit de mettre à jour les contraintes et d'appeler layoutIfNeeded à l'intérieur du bloc d'animation. Il peut s'agir de changer la propriété .constant d'un NSLayoutConstraint, d'ajouter des contraintes de suppression (iOS 7) ou de modifier la propriété .active de 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

On se demande si la contrainte doit être changée avant le bloc d’animation ou à l'intérieur de (voir 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 bien que changer des constantes en dehors du bloc d'animation puisse actuellement fonctionner, ce n'est pas sûr et elles devraient vraiment être changées à l'intérieur du 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 modifiant la propriété .active de plusieurs contraintes. https://github.com/shepting/SampleAutoLayoutAnimation

67
Steven Hepting
// 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];
}];
34
John Erck

Solution Swift 4

IView.animate

Trois étapes simples:

  1. Modifier les contraintes, par exemple:

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

    self.view.setNeedsLayout()
    
  3. Dans bloc d'animation, indiquez à la présentation de recalculer celle-ci, ce qui revient à définir les images directement (dans ce cas, l'autolayout définira les images):

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

Complet exemple le plus simple:

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

Sidenote

Il existe une étape facultative - avant de modifier les contraintes, vous pouvez appeler self.view.layoutIfNeeded() pour vous assurer que le point de départ de l'animation provient de l'état auquel les anciennes contraintes ont été appliquées (au cas où d'autres modifications de contraintes devraient être appliquées). ne 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()
}

IViewPropertyAnimator

Comme avec iOS 10, nous avons un nouveau mécanisme d’animation - UIViewPropertyAnimator, nous devrions savoir qu’en principe, le même mécanisme s’applique. 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()

Puisque animator est une encapsulation de l'animation, nous pouvons continuer à y faire référence et l'appeler plus tard. Cependant, puisque dans le bloc d'animation, nous demandons simplement à l'autolayout de recalculer les images, nous devons modifier 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 ne modifions que les contraintes et laissons notre animateur pour un point ultérieur, le cycle de redessinage suivant peut invoquer le recalcul automatique et le changement ne sera pas animé.

De plus, rappelez-vous qu'un seul animateur est non réutilisable - une fois que vous l'exécutez, 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 nous l'utilisons pour contrôler une animation interactive.

22
Milan Nosáľ

Storyboard, code, astuces et quelques pièges

Les autres réponses sont très bien, mais celle-ci met en évidence quelques pièges assez importants d'animation de contraintes à l'aide d'un exemple récent. Je suis passé par beaucoup de variations avant que je réalise ce qui suit:

Transformez les contraintes que vous souhaitez cibler en variables de classe afin de conserver 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ériences, j'ai remarqué qu'il fallait OBTENIR la contrainte de la vue CI-DESSUS (autrement dit le 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 pour lesquels je crée une contrainte et constituent 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 souhaitée qui servira de point d'inflexion. On pourrait aussi devenir beaucoup plus compliqué mais le filtre fait du bon travail ici.

Animation de contraintes à l'aide de Swift

Nota Bene - Cet exemple est la solution de storyboard/code et suppose que l’on a créé des contraintes par défaut dans le storyboard. On peut ensuite animer les modifications à l'aide de code.

En supposant que vous créiez une propriété à filtrer avec des critères précis et atteignez un point d'inflexion spécifique pour votre animation (vous pouvez bien entendu également filtrer un tableau et effectuer 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 qu'il ne faut pas faire personnellement et douloureusement. Espérons que ce guide pourra épargner les autres.

  1. Attention au zPositionnement. Parfois, lorsque rien ne semble se produire, vous devez masquer certaines des autres vues ou utiliser le débogueur de vues pour localiser votre vue animée. J'ai même trouvé des cas où un attribut d'exécution défini par l'utilisateur avait été perdu dans le XML d'un storyboard et avait pour effet de couvrir la vue animée (tout en travaillant).

  2. Prenez toujours une minute pour lire la documentation (nouvelle et ancienne), l’aide rapide et les en-têtes. Apple continue d'apporter de nombreuses modifications afin de mieux gérer les contraintes de mise en forme automatique (voir Vues de la pile). Ou au moins le AutoLayout Cookbook . N'oubliez pas que les meilleures solutions se trouvent parfois dans l'ancienne documentation/vidéo.

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

  4. Ne codez pas en dur des valeurs de présentation spécifiques en tant que critères pour déterminer les modifications apportées aux autres constantes, utilisez plutôt des valeurs vous permettant 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 associées à une vue participant à la définition de contrainte let viewMargins = self.webview.layoutMarginsGuide: is on example
  6. Ne faites pas de travail inutile, toutes les vues ayant des contraintes sur le storyboard ont des contraintes attachées à la propriété self.viewName.constraints.
  7. Modifiez vos priorités pour toutes les contraintes à moins de 1 000. Je règle le mien à 250 (faible) ou 750 (élevé) sur le scénarimage; (Si vous essayez de changer une priorité 1000 en n'importe quoi dans le code, l'application va planter car 1000 est requise)
  8. N'essayez pas immédiatement d'utiliser ActivateConstraints et deactivateConstraints (ils ont leur place, mais juste en apprenant ou si vous utilisez un storyboard, cela signifie probablement que vous en faites trop, ils ont une place, comme on peut le voir ci-dessous).
  9. Envisagez de ne pas utiliser addConstraints/removeConstraints, à moins que vous ajoutiez réellement une nouvelle contrainte dans le code. J'ai constaté que la plupart du temps, j'organisais les vues dans le storyboard avec les contraintes souhaitées (en plaçant la vue hors écran), puis en code, j'animais les contraintes précédemment créées dans le storyboard pour déplacer la vue.
  10. J'ai passé beaucoup de temps perdu à créer des contraintes avec la nouvelle classe et les sous-classes NSAnchorLayout. Celles-ci fonctionnent très bien mais il m'a fallu un certain temps pour comprendre que toutes les contraintes dont j'avais besoin existaient déjà dans le scénarimage. Si vous construisez des contraintes en code, alors utilisez certainement cette méthode pour agréger vos contraintes:

Exemple 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({

            ($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 des conseils plus simples, tels que l’ajout du layoutIfNeeded, rien ne se passera probablement: Dans ce cas, vous pouvez utiliser une solution à moitié cuite comme celle-ci:

NB - Prenez un moment pour lire la section Mise en forme automatique ci-dessous et le guide d'origine. Il existe 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 de mise en forme automatique (notez que le deuxième extrait concerne l’utilisation d’OS X). BTW - Ce n’est plus dans le guide actuel, à ma connaissance. 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 apportées par la mise en page automatique, vous devez apporter les modifications de contrainte nécessaires par programme. Le concept de base est identique pour iOS et OS X, à quelques différences mineures près.

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 lorsque vous utilisez des animations sauvegardées sur des couches:

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

Lorsque vous n’utilisez pas d’animations sauvegardées sur des calques, vous devez animer la constante à l’aide de l’animateur de la contrainte:

[[constraint animator] setConstant:42];

Pour ceux qui apprendront mieux visuellement, jetez un œil à 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 mènent à de plus grandes idées. Par exemple, lier des contraintes de mise en page automatique à des animateurs dynamiques est une grande idée.

Bonne chance et que la force soit avec toi.

13
Tommie C.

Solution rapide:

yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
    yourView.layoutIfNeeded
})
6
Nazariy Vlizlo

Solution de travail 100% Swift 3.1

j'ai lu toutes les réponses et je souhaite 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 devez les vérifier sur les appareils plus lents, par exemple iPhone 5, à 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
C0mrade

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

Ce que les 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 chaque NSLayoutConstraint que vous souhaitez animer.

J'ai créé un exemple dans GitHub .

4
Gabriel.Massana

Solution fonctionnant 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 à une customView (CalendarView). J'ai appelé le .layoutIfNeeded () sur self.view et PAS sur self.calendarView

J'espère que cette aide.

4
Edoardo Vicoli

Il y a un article qui parle de ça: 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 aide.

3
D.D.

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

La contrainte a défini un espace supérieur d'un champ de texte au sommet du conteneur. Lors de l’ouverture du clavier, je divise simplement la constante par 2.

Je ne pouvais pas réaliser une animation de contrainte lisse cohérente directement dans la notification au clavier. Environ la moitié de la durée d'affichage passerait à sa nouvelle position - sans animation.

Je me suis dit qu'il pourrait y avoir une mise en page supplémentaire résultant de l'ouverture du clavier. L'ajout d'un simple bloc dispatch_after avec un délai de 10 ms faisait que l'animation était exécutée à chaque fois - pas de saut.

1
ivanferdelja