Crear animaciones es genial. Son una parte importante de las
pautas de interfaz humana de iOS . Las animaciones ayudan a llamar la atención del usuario hacia cosas importantes o simplemente hacen que la aplicación sea menos aburrida.
Hay varias formas de implementar animación en iOS. Probablemente la forma más popular es usar
UIView.animate (withDuration: animaciones :) . Puede animar una capa de imagen con
CABasicAnimation . Además, UIKit le permite configurar animaciones personalizadas para mostrar el controlador utilizando
UIViewControllerTransitionDelegate .
En este artículo quiero discutir otra forma emocionante de animar vistas:
UIViewPropertyAnimator . Esta clase proporciona muchas más funciones de administración que su predecesora,
UIView.animat e. Úselo para crear animaciones temporales, interactivas e interrumpidas. Además, es posible cambiar rápidamente el animador.
Presentación de UIViewPropertyAnimator
UIViewPropertyAnimator se introdujo en
iOS 10 . Le permite crear animaciones de forma orientada a objetos. Veamos un ejemplo de una animación creada usando el
UIViewPropertyAnimator .

Así era cuando se usaba UIView.
UIView.animate(withDuration: 0.3) { view.frame = view.frame.offsetBy(dx: 100, dy: 0) }
Y aquí está cómo hacerlo con el
UIViewPropertyAnimator :
let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { view.frame = view.frame.offsetBy(dx:100, dy:0) } animator.startAnimation()
Si necesita verificar la animación, simplemente cree un área de juegos y ejecute el código como se muestra a continuación. Ambos fragmentos de código conducirán al mismo resultado.

Puede pensar que en este ejemplo no hay mucha diferencia. Entonces, ¿qué sentido tiene agregar una nueva forma de crear animaciones?
UIViewPropertyAnimator se vuelve más útil cuando necesita crear animaciones interactivas.
Animación interactiva e interrumpida
¿Recuerdas el clásico gesto de "Deslizar el dedo para desbloquear el dispositivo"? ¿O el gesto "Mueva el dedo en la pantalla de abajo hacia arriba" para abrir el Centro de control? Estos son excelentes ejemplos de animación interactiva. Puede comenzar a mover la imagen con el dedo, luego suéltela y la imagen volverá a su posición original. Además, puede capturar la imagen durante la animación y continuar moviéndola con el dedo.
Las animaciones de UIView no proporcionan una manera fácil de controlar el porcentaje de finalización de la animación. No puede pausar la animación en medio de un bucle y continuar su ejecución después de la interrupción.
En este caso, hablaremos sobre
UIViewPropertyAnimator . A continuación, veremos cómo puede crear fácilmente animación totalmente interactiva, interrumpida y animación inversa en unos pocos pasos.
Preparación del proyecto de lanzamiento.
Primero debes
descargar el proyecto de inicio .
Una vez que abra el archivo, encontrará la aplicación
CityGuide que ayuda a los usuarios a planificar sus vacaciones. El usuario puede desplazarse por la lista de ciudades y luego abrir una descripción detallada con información detallada sobre la ciudad que le gustó.
Considere el código fuente del proyecto antes de comenzar a crear hermosas animaciones. Esto es lo que puede encontrar en un proyecto abriéndolo en
Xcode :
- ViewController.swift : el controlador de aplicación principal con UICollectionView que muestra una matriz de objetos City .
- CityCollectionViewCell.swift: celda para mostrar la ciudad . De hecho, en este artículo, la mayoría de los cambios se aplicarán a esta clase. Puede notar que descriptionLabel y closeButton ya están definidos en la clase. Sin embargo, después de iniciar la aplicación, estos objetos estarán ocultos. No te preocupes, serán visibles un poco más tarde. Esta clase también tiene las propiedades collectionView e index . Más tarde serán utilizados para la animación.
- CityCollectionViewFlowLayout.swift: esta clase es responsable del desplazamiento horizontal. No lo cambiaremos todavía.
- City.swift : el modelo de aplicación principal tiene un método que se utilizó en ViewController.
- Main.storyboard: allí puede encontrar la interfaz de usuario para ViewController y CityCollectionViewCell .
Intentemos construir y ejecutar la aplicación de muestra. Como resultado de esto, obtenemos lo siguiente.
Implementación de despliegue y animación de colapso
Después de iniciar la aplicación, se muestra una lista de ciudades. Pero el usuario no puede interactuar con objetos en forma de celdas. Ahora debe mostrar información para cada ciudad cuando el usuario hace clic en una de las celdas. Echa un vistazo a la versión final de la aplicación. Esto es lo que realmente necesitaba ser desarrollado:
La animación se ve bien, ¿no? Pero no hay nada especial aquí, es solo la lógica básica del
UIViewPropertyAnimator . Veamos cómo implementar este tipo de animación. Cree un método
collectionView (_: didSelectItemAt) , agregue el siguiente fragmento de código al final del archivo
ViewController :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedCell = collectionView.cellForItem(at: indexPath)! as! CityCollectionViewCell selectedCell.toggle() }
Ahora necesitamos implementar el método de
alternar . Cambiemos a
CityCollectionViewCell.swift e implementemos este método.
Primero, agregue la enumeración del
Estado al principio del archivo, justo antes de la declaración de la clase
CityCollectionViewCell . Este listado le permite rastrear el estado de una celda:
private enum State { case expanded case collapsed var change: State { switch self { case .expanded: return .collapsed case .collapsed: return .expanded } } }
Agregue algunas propiedades para controlar la animación en la clase
CityCollectionViewCell :
private var initialFrame: CGRect? private var state: State = .collapsed private lazy var animator: UIViewPropertyAnimator = { return UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) }()
La variable
initialFrame se usa para almacenar el marco de la celda hasta que se
ejecuta la animación.
El estado se utiliza para rastrear si la celda está expandida o contraída. Y la variable
animador se usa para controlar la animación.
Ahora agregue el método de
alternar y llámelo desde el método de
cierre , por ejemplo:
@IBAction func close(_ sender: Any) { toggle() } func toggle() { switch state { case .expanded: collapse() case .collapsed: expand() } }
Luego agregamos dos métodos más:
expand () y
collapse () . Continuaremos su implementación. Primero, comenzamos con el método
expansiond () :
private func expand() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.initialFrame = self.frame self.descriptionLabel.alpha = 1 self.closeButton.alpha = 1 self.layer.cornerRadius = 0 self.frame = CGRect(x: collectionView.contentOffset.x, y:0, width: collectionView.frame.width, height: collectionView.frame.height) if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x -= 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x += 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = false collectionView.allowsSelection = false default: () } } animator.startAnimation() }
Cuanto codigo Déjame explicarte lo que está sucediendo paso a paso:
- Primero, verifique si collectionView e index no son iguales a cero. De lo contrario, no podremos iniciar la animación.
- Luego, comience a crear la animación llamando a animator.addAnimations .
- A continuación, guarde el cuadro actual, que se utiliza para restaurarlo en la animación de convolución.
- Luego establecemos el valor alfa para descriptionLabel y closeButton para que sean visibles.
- A continuación, quite la esquina redondeada y establezca un nuevo marco para la celda. La celda se mostrará en pantalla completa.
- A continuación, movemos las células vecinas.
- Ahora llame al método animator.addComplete () para deshabilitar la interacción de la imagen de la colección. Esto evita que los usuarios lo desplacen durante la expansión de la celda. También cambie el estado actual de la celda. Es importante cambiar el estado de la celda, y solo después de eso termina la animación.
Ahora agregue una animación de convolución. En resumen, es solo que restauramos la célula a su estado anterior:
private func collapse() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.descriptionLabel.alpha = 0 self.closeButton.alpha = 0 self.layer.cornerRadius = self.cornerRadius self.frame = self.initialFrame! if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x += 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x -= 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = true collectionView.allowsSelection = true default: () } } animator.startAnimation() }
Ahora es el momento de compilar y ejecutar la aplicación. Intente hacer clic en la celda y verá la animación. Para cerrar la imagen, haga clic en el icono de cruz en la esquina superior derecha.
Agregar procesamiento de gestos
Puede reclamar lograr el mismo resultado con
UIView.animate . ¿Cuál es el punto de usar un
UIViewPropertyAnimator ?
Bueno, es hora de hacer que la animación sea interactiva. Agregue un
UIPanGestureRecognizer y una nueva propiedad llamada
popupOffset para rastrear cuánto se puede mover la celda.
Declaremos estas variables en la clase
CityCollectionViewCell :
private let popupOffset: CGFloat = (UIScreen.main.bounds.height - cellSize.height)/2.0 private lazy var panRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(popupViewPanned(recognizer:))) return recognizer }()
Luego agregue el siguiente método para registrar la definición de deslizamiento:
override func awakeFromNib() { self.addGestureRecognizer(panRecognizer) }
Ahora necesita agregar el método
popupViewPanned para rastrear el gesto de deslizar. Pegue el siguiente código en
CityCollectionViewCell :
@objc func popupViewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: toggle() animator.pauseAnimation() case .changed: let translation = recognizer.translation(in: collectionView) var fraction = -translation.y / popupOffset if state == .expanded { fraction *= -1 } animator.fractionComplete = fraction case .ended: animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) default: () } }
Hay tres estados. Al comienzo del gesto, inicializamos el animador usando el método de
alternar e inmediatamente hacemos una pausa. Mientras el usuario arrastra la celda, actualizamos la animación configurando las propiedades del multiplicador de
fracciónCompleta . Esta es la magia principal del animador, que les permite controlar. Finalmente, cuando el usuario suelta su dedo, se llama al método animador continueAnimation para continuar la animación. Entonces la celda se moverá a la posición de destino.
Después de iniciar la aplicación, puede arrastrar la celda hacia arriba para expandirla. Luego arrastre la celda expandida hacia abajo para contraerla.
La animación ahora se ve bastante bien, pero no es posible interrumpir la animación en el medio. Por lo tanto, para que la animación sea completamente interactiva, debe agregar otra función: interrupción. El usuario puede iniciar la animación de expandir / contraer como de costumbre, pero la animación debe pausarse inmediatamente después de que el usuario haga clic en la celda durante el ciclo de animación.
Para hacer esto, debe guardar el progreso de la animación y luego tener en cuenta este valor para calcular el porcentaje de finalización de la animación.
Primero, declare una nueva propiedad en
CityCollectionViewCell :
private var animationProgress: CGFloat = 0
Luego actualice el bloque
.began del método
popupViewPanned con la siguiente línea de código para recordar el progreso:
animationProgress = animator.fractionComplete
En el bloque
.changed , debe actualizar la siguiente línea de código para calcular correctamente el porcentaje de finalización:
animator.fractionComplete = fraction + animationProgress
Ahora la aplicación está lista para probar. Ejecute el proyecto y vea qué sucede. Si todas las acciones se realizan correctamente siguiendo mis instrucciones, la animación debería verse así:
Animación inversa
Puede encontrar una falla para la implementación actual. Si arrastra un poco la celda y luego la devuelve a su posición original, la celda continuará expandiéndose cuando suelte el dedo. Arreglemos este problema para que la animación interactiva sea aún mejor.
Actualicemos el bloque
.end del método
popupViewPanned , como se describe a continuación:
let velocity = recognizer.velocity(in: self) let shouldComplete = velocity.y > 0 if velocity.y == 0 { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) break } switch state { case .expanded: if !shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } case .collapsed: if shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if !shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } } animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
Ahora tenemos en cuenta la velocidad del gesto para determinar si la animación debe invertirse.
Y finalmente, inserte otra línea de código en el bloque
.changed . Coloque este código a la derecha del cálculo
animator.fractionComplete .
if animator.isReversed { fraction *= -1 }
Ejecutemos la aplicación nuevamente. Ahora todo debería funcionar sin falta.
Fix pan gest
Entonces, hemos completado la implementación de la animación usando el
UIViewPropertyAnimator . Sin embargo, hay un error desagradable. Es posible que la hayas conocido mientras probabas la aplicación. El problema es que no es posible desplazar la celda horizontalmente. Intentemos deslizar hacia la izquierda / derecha a través de las celdas, y nos enfrentaremos al problema.
La razón principal está relacionada con el
UIPanGestureRecognizer que
creamos . También detecta un gesto de deslizamiento y entra en conflicto con el reconocedor de gestos
incorporado UICollectionView .
Aunque el usuario todavía puede desplazarse por la parte superior / inferior de las celdas o el espacio entre celdas para desplazarse por las ciudades, todavía no me gusta una interfaz de usuario tan mala. Vamos a arreglarlo
Para resolver conflictos, necesitamos implementar un método delegado llamado
gestRecognizerShouldBegin (_ :) . Este método controla si el reconocedor de gestos debe continuar interpretando toques. Si devuelve
falso en el método, el reconocedor de gestos ignorará los toques. Entonces, lo que vamos a hacer es dar a nuestra propia herramienta de reconocimiento de panorama la capacidad de ignorar los movimientos horizontales.
Para hacer esto, configuremos el
delegado de nuestro reconocedor de pan. Inserte la siguiente línea de código en la inicialización de
panRecognizer (puede poner el código justo antes del
reconocedor de retorno :
recognizer.delegate = self
Luego, implementamos el
método gestRecognizerShouldBegin (_ :) de la siguiente manera:
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return abs((panRecognizer.velocity(in: panRecognizer.view)).y) > abs((panRecognizer.velocity(in: panRecognizer.view)).x) }
Abriremos / cerraremos si su velocidad vertical es mayor que la velocidad horizontal.
Wow! Probemos la aplicación nuevamente. Ahora puede moverse por la lista de ciudades deslizando el dedo hacia la izquierda / derecha por las celdas.
Bonificación: características de sincronización personalizadas
Antes de terminar este tutorial, hablemos sobre las funciones de temporización. ¿Todavía recuerdas el caso cuando el desarrollador te pidió que implementaras una función de sincronización personalizada para la animación que creaste?
Por lo general, debe cambiar
UIView.animation a
CABasicAnimation o envolverlo en
CATransaction .
Con UIViewPropertyAnimator, puede implementar fácilmente funciones de temporización personalizadas.
Las
funciones de temporización (o funciones de aceleración) se entienden como funciones de velocidad de animación que afectan la velocidad de cambio de una u otra propiedad animada. Actualmente se admiten cuatro tipos: easeInOut, easeIn, easeOut, linear.
Reemplace la inicialización del animador con estas funciones de temporización (intente dibujar su propia curva cúbica de Bezier) de la siguiente manera:
private lazy var animator: UIViewPropertyAnimator = { let cubicTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.17, y: 0.67), controlPoint2: CGPoint(x: 0.76, y: 1.0)) return UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTiming) }()
Alternativamente, en lugar de usar parámetros de sincronización cúbica, también puede usar sincronización de resorte, por ejemplo:
let springTiming = UISpringTimingParameters(mass: 1.0, stiffness: 2.0, damping: 0.2, initialVelocity: .zero)
Intente iniciar el proyecto nuevamente y vea qué sucede.
Conclusión
A través de UIViewPropertyAnimator, puede mejorar las pantallas estáticas y la interacción del usuario a través de animaciones interactivas.
Sé que no puedes esperar para darte cuenta de lo que has aprendido en tu propio proyecto. Si aplica este enfoque en su proyecto, será muy bueno, hágamelo saber al dejar un comentario a continuación.
Como referencia, aquí puede
descargar el borrador final .
Enlaces adicionales
Animaciones profesionales con UIKit :
https://developer.apple.com/videos/play/wwdc2017/230/Documentación UIViewPropertyAnimator para desarrolladores de Apple :
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator