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.
Deux notes importantes:
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.
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()
}
J'apprécie la réponse fournie, mais je pense qu'il serait bien d'aller un peu plus loin.
[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
?
[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 UITextField
s 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.
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).
[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];
}];
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
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
// 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];
}];
Solution Swift 4
Trois étapes simples:
Modifier les contraintes, par exemple:
heightAnchor.constant = 50
Dites au view
contenant que sa mise en page est sale et que l'autolayout doit recalculer la mise en page:
self.view.setNeedsLayout()
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()
}
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.
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
}
}
}
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.
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).
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.
Jouez avec les valeurs de l'animation et envisagez d'utiliser d'autres variantes de animateWithDuration.
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
let viewMargins = self.webview.layoutMarginsGuide
: is on exampleExemple 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 .
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.
Solution rapide:
yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
yourView.layoutIfNeeded
})
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.
}
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.
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.
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.
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.