Pregunta ¿Cómo puedo animar los cambios de restricción?


Estoy actualizando una aplicación vieja con un AdBannerView y cuando no hay anuncio, se desliza fuera de la pantalla. Cuando hay un anuncio, se desliza en la pantalla. Cosas básicas.

Estilo antiguo, configuro el marco en un bloque de animación. Nuevo estilo, tengo un IBOutlet a la restricción que determina la posición Y, en este caso es la distancia desde la parte inferior de la supervista, y modificar la constante.

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

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

Y el banner se mueve, exactamente como se esperaba, pero sin animación.

ACTUALIZAR: Volví a ver el video de WWDC12 "Mejores prácticas para dominar el diseño automático"que cubre la animación. Discute cómo actualizar las restricciones usando CoreAnimation.

enter image description here enter image description here

He intentado con el siguiente código, pero obtengo exactamente los mismos resultados.

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

En una nota lateral, he comprobado en numerosas ocasiones y esto se está ejecutando en el hilo principal.


832
2017-09-27 13:23


origen


Respuestas:


Dos notas importantes:

  1. Tienes que llamar layoutIfNeeded dentro del bloque de animación. Apple realmente recomienda que lo llames una vez antes del bloque de animación para asegurarte de que todas las operaciones de diseño pendientes se hayan completado

  2. Debe llamarlo específicamente en el vista de los padres (p.ej. self.view), no la vista secundaria que tiene las restricciones asociadas. Al hacerlo, se actualizará todas vistas restringidas, incluida la animación de otras vistas que podrían estar restringidas a la vista que cambió la restricción de (por ejemplo, Vista B está adjunta a la parte inferior de la Vista A y acaba de cambiar la compensación superior de la Vista A y desea que la Vista B la anime)

Prueba esto:

C objetivo

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



Agradezco la respuesta brindada, pero creo que sería bueno llevarlo un poco más allá.

La animación de bloque básica de la documentación

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

pero realmente este es un escenario muy simplista. ¿Qué ocurre si quiero animar las restricciones de la subvista a través del updateConstraints ¿método?

Un bloque de animación que llama al método updateSonscriptions de las subvistas

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

El método updateConstraints se reemplaza en la subclase UIView y debe llamar a super al final del método.

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

La guía de AutoLayout deja mucho que desear, pero vale la pena leerlo. Yo mismo estoy usando esto como parte de un UISwitch que alterna una subvista con un par de UITextFields con una animación de colapso simple y sutil (0.2 segundos de largo). Las restricciones para la subvista se manejan en los métodos de actualización de subclases de UIView como se describió anteriormente.


96
2018-01-22 00:35



Generalmente, solo necesitas actualizar las restricciones y llamar layoutIfNeeded dentro del bloque de animación. Esto puede ser cambiar el .constant propiedad de un NSLayoutConstraint, agregar restricciones de eliminación (iOS 7) o cambiar el .active propiedad de restricciones (iOS 8 y 9).

Código de muestra:

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

Configuración de muestra:

Xcode Project so sample animation project.

Controversia

Hay algunas preguntas sobre si la restricción debería cambiarse antes de el bloque de animación, o dentro (ver respuestas anteriores).

La siguiente es una conversación de Twitter entre Martin Pilkington, que enseña iOS, y Ken Ferry, quien escribió Auto Layout. Ken explica que aunque cambie las constantes fuera del bloque de animación puede actualmente trabajo, no es seguro y realmente deberían ser cambios dentro el bloque de animación. https://twitter.com/kongtomorrow/status/440627401018466305

Animación:

Proyecto de muestra

Aquí hay un proyecto simple que muestra cómo se puede animar una vista. Está usando Objective C y anima la vista cambiando el .active propiedad de varias restricciones. 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, Tips y algunas Gotchas

Las otras respuestas están bien, pero esta resalta algunas capturas bastante importantes de restricciones de animación usando un ejemplo reciente. Pasé por muchas variaciones antes de darme cuenta de lo siguiente:

Establezca las restricciones que desea apuntar en variables de clase para mantener una referencia fuerte. En Swift usé variables perezosas:

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

Después de un poco de experimentación, noté que uno DEBE obtener la restricción de la vista ENCIMA (aka la supervista) las dos vistas donde se define la restricción. En el ejemplo a continuación (tanto MNGStarRating como UIWebView son los dos tipos de elementos en los que estoy creando una restricción, y son subvistas dentro de self.view).

Encadenamiento del filtro

Aprovecho el método de filtro de Swift para separar la restricción deseada que servirá como punto de inflexión. También podría ser mucho más complicado, pero el filtro hace un buen trabajo aquí.

Animar restricciones usando Swift

Nota Bene - Este ejemplo es la solución de guión gráfico / código y asume   uno ha hecho restricciones predeterminadas en el guión gráfico. Uno puede entonces   anima los cambios usando código.

Asumiendo que crea una propiedad para filtrar con criterios precisos y llega a un punto de inflexión específico para su animación (por supuesto, también puede filtrar por una matriz y recorrer si necesita múltiples restricciones):

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

....

Algún tiempo después...

@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

        }
    }
}

Los muchos giros incorrectos

Estas notas son realmente un conjunto de consejos que escribí para mí. Hice todo lo que no debo hacer personalmente y dolorosamente. Con suerte, esta guía puede ahorrarles a los demás.

  1. Tenga cuidado con zPositioning. A veces cuando aparentemente no hay nada pasando, debería ocultar algunas de las otras vistas o usar la vista depurador para ubicar su vista animada. Incluso encontré casos donde un tiempo de ejecución definido por el usuario El atributo se perdió en el xml de un guión gráfico y condujo a la animación vista cubierta (mientras trabaja).

  2. Siempre tómese un minuto para leer la documentación (nueva y antigua), Quick Ayuda y encabezados Apple sigue haciendo muchos cambios para mejorar gestionar las restricciones de AutoLayout (ver vistas de la pila). O al menos el AutoLayout Cookbook. Tenga en cuenta que a veces las mejores soluciones se encuentran en la documentación / videos anteriores.

  3. Juega con los valores de la animación y considera usar otras variantes de animateWithDuration.

  4. No codificar los valores de diseño específicos como criterios para determinar cambios en otras constantes, en su lugar use valores que le permitan determinar la ubicación de la vista. CGRectContainsRectes uno ejemplo

  5. Si es necesario, no dude en utilizar los márgenes de diseño asociados con una vista que participa en la definición de restricción let viewMargins = self.webview.layoutMarginsGuide: está en el ejemplo
  6. No haga el trabajo que no tiene que hacer, todas las vistas con restricciones en el el guión gráfico tiene restricciones asociadas a la propiedad self.viewName.constraints
  7. Cambie sus prioridades para cualquier restricción a menos de 1000. Configuré mina a 250 (bajo) o 750 (alto) en el guión gráfico; (Si intenta cambiar una prioridad de 1000 a cualquier cosa en el código, la aplicación se bloqueará porque se requiere 1000)
  8. Considere no intentar de inmediato el uso de activateConstraints y desactiva las Restricciones (tienen su lugar, pero cuando estás aprendiendo o si estás usando un guión gráfico, usarlas probablemente significa que estás haciendo demasiado ~ aunque tienen un lugar como se ve a continuación)
  9. Considere no usar addConstraints / removeConstraints a menos que sea realmente agregando una nueva restricción en el código. Encontré que la mayoría de las veces diseñar las vistas en el guión gráfico con las restricciones deseadas ( la vista fuera de la pantalla), luego en el código, yo animo las restricciones previamente creadas en el guión gráfico para mover la vista.
  10. Pasé mucho tiempo perdido acumulando restricciones con el nuevo Clase y subclases NSAnchorLayout. Estos funcionan bien, pero me tomó un tiempo darme cuenta de que todas las limitaciones que necesitaba ya existía en el guión gráfico. Si construyes restricciones en el código luego, sin duda, use este método para agregar sus restricciones:

Muestra rápida de soluciones para EVITAR al usar guiones gráficos

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 olvida uno de estos consejos o los más simples, como dónde agregar el diseño, si es necesario, lo más probable es que no ocurra nada: en ese caso, puede que tenga una solución medio horneada como esta:

NB - Tómese un momento para leer la sección de AutoLayout a continuación y la   guía original Hay una manera de usar estas técnicas para complementar   tus animadores dinámicos

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
        }

    }

Fragmento de la Guía de AutoLayout (tenga en cuenta que el segundo fragmento es para usar OS X). Por cierto, esto ya no está en la guía actual, por lo que puedo ver.  Las técnicas preferidas continúan evolucionando.

Animación de cambios realizados por diseño automático

Si necesita un control total sobre la animación de los cambios realizados por Diseño automático, debe realizar sus cambios de restricción mediante programación. El concepto básico es el mismo para iOS y OS X, pero hay algunas diferencias menores.

En una aplicación de iOS, su código se vería como el siguiente:

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

En OS X, use el siguiente código cuando use animaciones respaldadas por capas:

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

Cuando no está utilizando animaciones respaldadas por capa, debe animar la constante usando el animador de la restricción:

[[constraint animator] setConstant:42];

Para aquellos que aprenden mejor visualmente revisa esto temprano video de Apple.

Presta mucha atención

A menudo, en la documentación, hay pequeñas notas o fragmentos de código que conducen a ideas más grandes. Por ejemplo, adjuntar restricciones de diseño automático a animadores dinámicos es una gran idea.

Buena suerte y que la fuerza te acompañe.


10
2017-12-11 01:49



Solución Swift 4

UIView.animate

Tres simples pasos:

  1. Cambiar las restricciones, p. Ej .:

    heightAnchor.constant = 50
    
  2. Dile a los que contienen view que su diseño está sucio y que el diseño automático debería volver a calcular el diseño:

    self.view.setNeedsLayout()
    
  3. En el bloque de animación, dígale al diseño que recalcule el diseño, lo que es equivalente a establecer los marcos directamente (en este caso, el ajuste automático establecerá los marcos):

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

Complete el ejemplo más simple:

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

Nota al margen

Hay un décimo paso opcional: antes de cambiar las restricciones a las que desea llamar self.view.layoutIfNeeded() para asegurarse de que el punto de partida de la animación sea del estado con las antiguas restricciones aplicadas (en caso de que haya otros cambios de restricciones que no deberían incluirse en la animación):

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

Dado que con iOS 10 tenemos un nuevo mecanismo de animación: UIViewPropertyAnimator, debemos saber que, básicamente, se aplica el mismo mecanismo. Los pasos son básicamente los mismos:

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

Ya que animator es una encapsulación de la animación, podemos seguir haciendo referencia a ella y llamarla más tarde. Sin embargo, dado que en el bloque de animación solo le decimos a la función de autodiseño que recalcule los cuadros, tenemos que cambiar las restricciones antes de llamar startAnimation. Por lo tanto, algo como esto es posible:

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

El orden de las restricciones cambiantes y el inicio de un animador es importante: si simplemente cambiamos las restricciones y dejamos nuestro animador para un punto posterior, el próximo ciclo de redibujado puede invocar el recálculo de la autoimpresión y el cambio no se animará.

Además, recuerde que un único animador no es reutilizable: una vez que lo ejecuta, no puede "volver a ejecutarlo". Así que supongo que no hay realmente una buena razón para mantener al animador cerca, a menos que lo usemos para controlar una animación interactiva.


7
2018-02-16 11:09



Solución Swift:

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

6
2017-08-11 08:42



Solución de trabajo 100% Swift 3.1

He leído todas las respuestas y quiero compartir el código y la jerarquía de las líneas que he usado en todas mis aplicaciones para animarlas correctamente. Algunas soluciones aquí no funcionan, debe verificarlas en dispositivos más lentos como el iPhone 5 en este momento.

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



Estaba intentando animar Restricciones y no fue realmente fácil encontrar una buena explicación.

Lo que otras respuestas dicen es totalmente cierto: debes llamar [self.view layoutIfNeeded]; dentro animateWithDuration: animations:. Sin embargo, el otro punto importante es tener punteros para cada NSLayoutConstraint quieres animar

Creé un ejemplo en GitHub.


4
2017-11-25 08:24



Solución de trabajo y recién probada para Swift 3 con 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)

Solo tenga en cuenta que self.calendarViewHeight es una restricción referida a una vista personalizada (Vista de calendario). Llamé a .layoutIfNeeded () en self.view y NO en self.calendarView

Espero que esto ayude.


3
2017-07-19 12:17