Animaciones con SwiftUI

0
3443
  1. Introducción
  2. Entorno
  3. Animaciones básicas en SwiftUI
  4. Transiciones
  5. Springs
  6. Animatable
  7. Otras maneras de hacer las animaciones
  8. Conclusiones
  9. Referencias

Introducción

SwiftUI trae una forma más fácil de escribir código de UI en todas las plataformas de Apple. Como beneficio adicional, también tiene una nueva forma de animar las transiciones de estado. Si estás acostumbrad@ a las animaciones UIView, puedes encontrarlas más fáciles de escribir y de comprender. Si estás empezando aprender SwiftUI, ¡felicidades! Este tutorial te permitirá easyIn. ¡Es una broma de animación! 🙂

En este tutorial, aprenderás los conceptos básicos de la animación SwiftUI, que incluyen:

  • el modificador animation.
  • withAnimation, el método que te permite animar cambios de estado.
  • las animaciones customizadas en SwiftUI

Entorno

Este tutorial está escrito usando el siguiente entorno:

    • Hardware: MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3, mediados de 2015)
    • Sistema operativo: macOS Catalina 10.15
    • Versiones del software:
      • Xcode: 11
      • iOS SDK: 13.4

Es importante saber que para probar SwiftUI se necesita instalar Mac OS Catalina 10.15 y Xcode 11.

Animaciones básicas en SwiftUI

Para agregar animaciones, lo primero que necesitas es algo para animar. Entonces, para comenzar, necesitas crear un cambio de estado que desencadene una actualización de la interfaz de usuario.
Puedes añadir el modificador .animation a cualquier cadena de modificadores que cambian las propiedades de la vista. Lo importante aquí es cambiar el valor después de que la vista aparezca. Se puede hacerlo con el modificador .onAppear.

Text("\(character.name ?? "")")
.scaleEffect(self.animationStarted ? 1 : 5)
.animation(.default).onAppear{
   self.animationStarted = true
}

Después del modificador scaleEffect añade el siguiente modificador .animation(.default). Esto añade una animación predeterminada al efecto de escala.

Animation Timing

El modificador de animación .animation(_ 🙂 toma un parámetro de tipo Animation.
Hay varias opciones que puedes usar para una animación. Las básicas son curvas de tiempo simples que describen cómo cambia la velocidad de la animación a lo largo de la duración. Todas las opciones modifican los atributos, interpolando suavemente entre los valores inicial y final.

Tienes las siguientes opciones:

  • .linear: transita el atributo del valor inicial al final de manera uniforme a lo largo del tiempo. Esta es una buena curva de tiempo para repetir animaciones en SwiftUI, pero no parece tan natural como otras opciones.

  • .easeIn: una animación comienza lentamente y aumenta la velocidad con el tiempo. Sirve para animaciones que comienzan desde un punto de descanso y terminan fuera de la pantalla.

  • .easeOut: las animaciones comienzan rápido y terminan lento. Es buena para animar algo que llega a un estado estable o una posición final.

  • .easeInOut: combina la facilidad y la rapidez. Está guay para las animaciones que comienzan en un punto estable y terminan en otro equilibrio. Se puede utilizarla en la mayoría de los casos. Es por eso es la curva de tiempo utilizada por .default.

  • .timingCurve: te permite especificar una curva de tiempo personalizada. Es raramente necesaria y está fuera del alcance de este tutorial.

La mayoría de las veces, .default será lo suficientemente bueno para tus necesidades. Si necesitas algo más, puedes cambiarla a una de estas y te darán un poco de refinamiento adicional.

Jugar con el tiempo

Si tienes problemas para ver las diferencias sutiles, puedes ralentizar la animación haciendo que tome más tiempo. Reemplaza el modificador de animación con:

.animation(.easeIn(duration: 5))

Especificar una duración más larga hará que la curva de tiempo sea más notable.
Una ventaja de construir y ejecutar en el simulador en lugar de la ventana de vista previa de SwiftUI es que puedes habilitar el indicador Debug ▸ Slow Animations. Esto ralentizará drásticamente cualquier animación para que puedas ver las diferencias sutiles más claramente. De esta manera, no tienes que agregar parámetros de duración adicionales.
Puedes usar algunas otros parámetros para controlar el tiempo de una animación, además de la curva de tiempo y la duración. Primero, la velocidad:

Animation.easeInOut.speed(0.1)

Además de la velocidad, también puedes agregar demora:

Animation.easeInOut.delay(1)

Ahora, la animación tiene un retraso de un segundo. La demora es más útil al animar varias propiedades u objetos a la vez.

Finalmente, puedes usar modificadores para repetir una animación. Cambia la animación a:

Animation.easeInOut.repeatForever(autoreverses: true)

Ahora la animación será permanente. También puedes usar repeatCount (autoreverses 🙂 para repetir la animación un número limitado de veces.

Animaciones Simultáneas en SwiftUI

.animation es un modificador que se añade a una vista SwiftUI como cualquier otro. Si una vista tiene múltiples atributos cambiantes, se puede aplicar una sola animación a todos ellos.
Añade el siguiente efecto de rotación colocándolo entre las líneas Image y scaleEffect:

Text("\(character.name ?? "")")
 .scaleEffect(self.animationStarted ? 1 : 5)
 .rotationEffect(.degrees(self.animationStarted ? 0 : -50))
 .animation(.default)
 .onAppear{
    self.animationStarted = true
}

Esto añade una pequeña rotación al texto a la vez disminuyendo el tamaño del texto. Las dos animaciones se ejecutan juntas ya que has añadido el modificador .animation al final.

Por supuesto, puedes especificar animaciones separadas para cada atributo. Por ejemplo, agrega el siguiente modificador después del modificador de efecto de rotación:

Text("\(character.name ?? "")")
   .rotationEffect(.degrees(self.animationStarted ? 0 : -50))
   .animation(.easeOut(duration: 1))
   .scaleEffect(self.animationStarted ? 1 : 5)
   .animation(.default)
   .onAppear{
       self.animationStarted = true
 }

Esto le da a la rotación una animación de un segundo, por lo que notarás que la rotación termina un poco más tarde en comparación con el efecto de escala. A continuación, puedes cambiar la linea con .animation(.default) a:

.animation(Animation.default.delay(1))

Esto retrasa la animación en un segundo, por lo que la escala comienza después de que finalice la rotación.

Finalmente, puedes optar por no animar un cambio de atributo particular especificando nil para la animación.

.animation(nil)

Animando cambios de estado

Pero si necesitas algo más sofisticado y complicado puedes usar un método withAnimation. De este modo, puedes animar cualquier transición de estado con un simple bloque de animación. Reemplaza el contenido del bloque de acción del botón con:

Text("\(character.name ?? "")").frame(maxWidth: .infinity, alignment: Alignment.leading)
.rotationEffect(.degrees(self.animationStarted ? 0 : -50))
.scaleEffect(self.animationStarted ? 1 : 5)
.onAppear{
    withAnimation {
        self.animationStarted = true
  }
}

El método withAnimation dice explícitamente a SwiftUI qué animar. En este caso, anima el cambio de self.animationStarted y cualquier vista que tenga atributos que dependan de ella.
También puedes elegir una animación específica al bloque de animación explícito. Utilizando withAnimation así:

withAnimation(.easeIn(duration: 2))

Esto ahora usa una animación simplificada con una duración de dos segundos en lugar de la predeterminada.

Transiciones en SwiftUI

Una transición define cómo se inserta o quita una vista. Para ver cómo funciona, tienes que añadir el modificador .transition(.customTransition):

Image(“driving”)
.transition(AnyTransition.slide) //NO FUNCIONA!!

Pero no pasa nada, no vemos ningún efecto.

Text("\(character.name ?? "")").frame(alignment: Alignment.leading)
.transition(.slide)
.animation(.default)

Tampoco pasa nada. ¿nos falta algo?

Es importante tener en cuenta que una vista con los modificadores de una transición y de una animación todavía no tendrá efecto.

if self.animationStarted {
      Text("\(character.name ?? "")").frame(alignment: Alignment.leading)
      .transition(.slide)
      .animation(.default)
}

Tienes que agregar la vista que tiene la transición a la pantalla (y, opcionalmente, eliminarla de la pantalla) para ver el efecto.

Ahora, sí vemos el efecto. El texto se desliza desde izquierda y va a la derecha.
Las transiciones son del tipo AnyTransition. SwiftUI viene con algunas transiciones predeterminadas:

  • .slide: ya has visto este en acción: desliza la vista desde un lado.
  • .opacity: esta transición desvanece la vista dentro y fuera.
  • .scale: anima al ampliar o reducir la vista.
  • .move: es como una diapositiva, excepto que puede especificar el borde.
  • .offset: mueve la vista en una dirección.

Sigue adelante y prueba algunas de estas transiciones para tener una idea de cómo funcionan.

Combinando transiciones

También puedes combinar transiciones para componer sus propios efectos personalizados. En la parte superior de ContentView.swift, agrega esta extensión:

extension AnyTransition {
  static var customTransition: AnyTransition {
    let transition = AnyTransition.move(edge: .top)
      .combined(with: .scale(scale: 0.2, anchor: .topTrailing))
      .combined(with: .opacity)
    return transition
  }
}

Esto combina tres transiciones: un movimiento desde el borde superior, una escala del 20% anclada a la esquina superior y un efecto de atenuación de opacidad.
Para usarlo, cambia la línea de transición a:

.transition(.customTransition)

El efecto combinado es como si el texto entrara y saliera de arriba para abajo.

Transiciones Asincrónicas

Si lo deseas, también puedes hacer que su transición de entrada sea diferente de su transición de salida.

static var customTransition: AnyTransition {
  let insertion = AnyTransition.move(edge: .top)
    .combined(with: .scale(scale: 0.2, anchor: .topTrailing))
    .combined(with: .opacity)
  let removal = AnyTransition.move(edge: .top)
  return .asymmetric(insertion: insertion, removal: removal)
}

En este caso mantenemos la inclinación en la inserción, pero ahora la vista sale de la pantalla moviéndose hacia la parte superior.

Springs

Las animaciones predefinidas aún no son muy perfectas, por lo que deberás agregar un poco más de dinamismo. Las animaciones de resorte (spring) te permiten agregar un pequeño rebote y sacudidas para que sus vistas se sientan vivas.

func animation(index: Double) -> Animation {
  return Animation.spring(dampingFraction: 0.5)
}

Como todo lo demás en SwiftUI, hay algunas opciones integradas para resortes:

  • spring(): Comportamiento predeterminado. Esto es un buen punto de partida.
  • spring(response:dampingFraction:blendDuration): Una animación de resorte con más opciones para ajustar su comportamiento.
  • interpolatingSpring(mass:stiffness:damping:initialVelocity:): Un resorte muy personalizable basado en el modelo físico.

Las animaciones de resorte en SwiftUI utilizan la física del mundo real de los resortes como base. Imagínate, una bobina de resorte colgante con un bloque pesado unido al extremo. Si ese bloque tiene una gran masa, liberar el resorte causará un gran desplazamiento a medida que cae, haciendo que rebote más y más.
Un resorte más rígido tiene el efecto contrario: cuanto más rígido sea el resorte, más lejos viajará. Aumentar la amortiguación es como aumentar la fricción: dará como resultado menos recorrido y un período de rebote más corto. La velocidad inicial establece la velocidad de la animación: una velocidad mayor moverá más la vista y hará que rebote más.

Puede ser difícil entender cómo funcionará realmente la animación. Por eso es útil jugar con los parámetros para obtener una buena animación.

Esta es también la razón por la cual existe un método spring(response: dampingFraction: blendDuration), que es la forma más fácil de escribir una animación de Spring. Debajo del capó, todavía usa el modelo físico, pero tiene menos parámetros.

El parámetro dampingFraction controla cuánto tiempo rebotará la vista. Si el dampingFraction (amortiguación) es 0, es un sistema no amortiguado, lo que significa que rebotará para siempre. Un valor de dampingFraction que sea más de 1 no saltará en absoluto. Si estás utilizando un resorte, generalmente elegirás un valor en algún lugar entre 0 y 1. Los valores más grandes se ralentizarán más rápido.

El parámetro response es la cantidad de tiempo para completar una sola oscilación. Esto controlará cuánto dura la animación. Estos dos valores trabajan juntos para ajustar a dónde y cómo de rápido y, con que frecuencia, la vista rebotará. BlendDuration afecta a la animación si cambias el response o combinas varios resortes.

Ahora que tienes una mejor idea de cómo funcionan los resortes, tómate un momento para jugar con los parámetros. Una cosa que es bueno hacer cuando estás animando un montón de vistas relativamente independientes es darles tiempos ligeramente diferentes. Esto ayuda a que tu animación se sienta más viva. Por ejemplo, puedes ajustar la demora de cada animation dependiente del índice de cada vista.

func animation(index: Double) -> Animation {
  return Animation
    .spring(response: 0.55, dampingFraction: 0.45, blendDuration: 0)
    .speed(2)
    .delay(0.075 * index)
}

Animatable

Detrás de todas las animaciones de SwiftUI, hay un protocolo llamado Animatable. Entraremos en los detalles más adelante, pero principalmente, implica tener una propiedad calculada con un tipo que implemente el protocolo VectorArithmetic. Esto hace posible que el framework interpole los valores.

Al animar una vista, SwiftUI realmente está regenerando la vista muchas veces y modificando cada vez el parámetro de animación. De esta manera va progresivamente desde el valor de origen hasta el valor final.

Puedes hacerlo utilizando un modificador personalizado, un GeometryEffect. Los efectos de geometría describen una animación que modifica la posición, forma o tamaño de una vista.

func effectValue(size: CGSize) -> ProjectionTransform

Digamos que el método se llama SkewEffect, para aplicarlo a una vista, tienes que hacerlo así:

Text("Hello").modifier(SkewEfect(skewValue: 0.5))

El texto («Hello») se transformará con la matriz creada por el método SkewEfect.effectValue (). Tan simple como eso. Ten en cuenta que los cambios afectarán la vista, pero sin afectar al layout de sus antepasados o descendientes.

Debido a que GeometryEffect también implementa el protocolo Animatable, puedes agregar una propiedad animatableData, y ya está, tienes un efecto animatable.

Puede que no lo sepas, pero probablemente estés usando GeometryEffect todo el tiempo. Si alguna vez usaste .offset (), en realidad estabas usando GeometryEffect. Déjame mostrarte cómo se implementa:

public extension View {
    func offset(x: CGFloat, y: CGFloat) -> some View {
        return modifier(_OffsetEffect(offset: CGSize(width: x, height: y)))
    }

    func offset(_ offset: CGSize) -> some View {
        return modifier(_OffsetEffect(offset: offset))
    }
}

struct _OffsetEffect: GeometryEffect {
    var offset: CGSize
    
    var animatableData: CGSize.AnimatableData {
        get { CGSize.AnimatableData(offset.width, offset.height) }
        set { offset = CGSize(width: newValue.first, height: newValue.second) }
    }

    public func effectValue(size: CGSize) -> ProjectionTransform {
        return ProjectionTransform(CGAffineTransform(translationX: offset.width, y: offset.height))
    }
}

Este GeometryEffect mueve su vista a una distancia. Lo hace a través del método effectValue (size:) de GeometryEffect. Este método devuelve una ProjectionTransform, que es una transformación de coordenadas. Los datos animables pueden ser cualquier valor de tipo que implemente VectorArithmetic, lo que significa que se pueden interpolar entre un valor inicial y un valor final.

Otras maneras de hacer las animaciones en SwiftUI

Los tipos de animación que has visto hasta ahora suceden implícitamente o explícitamente cuando una vista se cambia debido a un cambio de estado. También puedes animar una vista controlando explícitamente los atributos de una vista. Por ejemplo, puedes realizar un cambio en el offset de una vista para moverla de una ubicación a otra.

Puedes darle vida a la lista dándole una interfaz similar a CoverFlow usando un efecto rotation3D.

VStack {
                GeometryReader{ viewGeometry in
                    ScrollView(.horizontal, showsIndicators: false){
                        GeometryReader{ imageGeometry in
                            AsyncImage(
                                url: self.character?.thumbnailUrl(),
                                cache: self.cache,
                                placeholder: LoadingView()
                            )
                            .frame(width: 300, height: 300)
                            .clipShape(Circle())
                            .shadow(radius: 10)
                            .padding(15)
                            .rotation3DEffect(.degrees(Double(imageGeometry.frame(in: .global).midX - viewGeometry.size.width / 2)),
                             axis: (x: 0, y: 1, z: 0))
                        }
                    }
                }

Esto aplica un efecto de rotación a lo largo del eje y.

Mediante la combinación de GeometryReader y un efecto como projectionEffect, transformEffect, scaleEffect o rotationEffect, puedes modificar la posición y la forma de una vista a medida que cambia la posición en la pantalla.

Conclusiones

Acabas de tocar solo algunas cosas que se puede hacer con las animaciones en SwiftUI. Afortunadamente, agregando un .animation (.default) o .animation (.spring ()) normalmente te llevará bastante lejos. Pero si necesitas algo mas elaborado, las animaciones en Swift es un mundo! Con la ayuda de GeometryEffect, GeometryReader o incluso Metal puedes hacer casi cualquier cosa.

Referencias

  1. Animation Views and transitions
  2. Documentation about Drawing and Animation
  3. Advanced SwiftUI Animations

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad