Sumérjase más en Motion Design: transiciones avanzadas de iOS
Publicado: 2020-11-09La creatividad trae consigo un deseo constante de impresionar al usuario. Durante siglos, el hombre ha tratado de manipular los medios disponibles para recrear la interacción más natural, tomando a la naturaleza como ejemplo fundamental.
Al descubrir el mundo, una persona se vuelve cada vez más sensible a los detalles sutiles del mundo que la rodea, lo que le permite distinguir instintivamente la artificialidad de los seres vivos. Esta línea se desdibuja con el desarrollo de la tecnología, donde el software tiene como objetivo crear un entorno en el que su usuario describe su experiencia en un mundo creado artificialmente como natural.

Haciéndose eco de la naturaleza en el diseño de la aplicación
Este artículo presentará el proceso de difuminar el borde utilizando el ejemplo de transformación de forma en animación interactiva de elementos cotidianos en la mayoría de las aplicaciones de iOS. Una forma de imitar la naturaleza es a través de diversas transformaciones de la posición de un objeto en el tiempo. Las funciones ejemplares del tiempo de animación se presentan a continuación.

Combinado con el uso adecuado del tiempo mediante la adición de una transformación geométrica, podemos obtener una infinidad de efectos . Como demostración de las posibilidades del diseño y la tecnología actual, se creó la aplicación Motion Patterns, que incluye soluciones populares, desarrolladas por nuestra empresa de desarrollo de software. Como no soy escritor, sino programador, y nada habla mejor que los ejemplos en vivo, ¡no tengo más remedio que invitarlos a este maravilloso mundo!
Muestras para descubrir

¡Echemos un vistazo a cómo el diseño de movimiento puede transformar un diseño corriente en algo excepcional! En los ejemplos a continuación, en el lado izquierdo hay una aplicación que usa solo animaciones básicas de iOS, mientras que en el lado derecho hay una versión de la misma aplicación con algunas mejoras.
Efecto "Sumérgete más profundo"
Esta es una transición usando transformación entre dos estados de una vista . Construido sobre la base de una colección, después de seleccionar una celda específica, la transición a los detalles de un elemento se lleva a cabo mediante la transformación de sus elementos individuales *. Una solución adicional es el uso de transiciones interactivas, que facilitan el uso de la aplicación.
*realmente copiando/asignando elementos de datos en una vista temporal tomando parte en la transición, entre su inicio y su fin... pero explicaré esto más adelante en este artículo...
Efecto “Mirar por el borde”
El uso de la animación de vista de desplazamiento en su acción transforma la imagen en forma de 3D para un efecto de cubo . El principal factor responsable del efecto es el desplazamiento de la vista de desplazamiento.
Efecto “Conectar los puntos”
Esta es una transición entre escenas que transforma el objeto en miniatura en la pantalla completa . Las colecciones utilizadas para este propósito funcionan simultáneamente, un cambio en una pantalla corresponde a un cambio en la otra. Además, cuando ingresas a la miniatura en el fondo, aparece un efecto de paralaje cuando pasas de una escena a otra.
Efecto "Cambiar la forma"
El último tipo de animación es simple y utiliza la biblioteca Lottie. Es el uso más común para animar iconos. En este caso, estos son los iconos de la barra de pestañas. Además, al cambiar las pestañas apropiadas, se utilizó una animación de la transición en una dirección específica para intensificar aún más el efecto de interacción.
Sumérgete más profundo: nuestro primer patrón de diseño de movimiento
Ahora es el momento de ir al grano... tenemos que profundizar aún más en la estructura de los mecanismos que controlan estos ejemplos.
En este artículo, te presentaré el primer patrón de diseño de movimiento , al que llamamos 'Dive Deeper' con una descripción abstracta de su uso, sin entrar en detalles específicos. Planeamos hacer que el código exacto y todo el repositorio estén disponibles para todos en el futuro, sin restricciones.
La arquitectura del proyecto y los estrictos patrones de diseño de programación aplicados no son una prioridad en este momento: nos centramos en las animaciones y la transición.
En este artículo, usaremos dos conjuntos de funciones que se proporcionan para administrar las vistas durante las transiciones de escena. Por lo tanto, me gustaría señalar que este artículo está dirigido a personas que están relativamente familiarizadas con UIKit y la sintaxis de Swift.
https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning
https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html
Primero: la estructura
Para la versión básica que implemente una solución dada, se necesitarán varias clases de ayuda, responsables de proporcionar la información necesaria sobre las vistas involucradas en la transición y controlar la transición en sí y las interacciones.

La clase básica responsable de gestionar la transición, el proxy, será TransitionAnimation. Decide de qué manera se llevará a cabo la transición y cubre las funciones estándar necesarias para realizar la acción proporcionada por el equipo de Apple.
/// Esta es una clase fundamental para las transiciones que tienen un comportamiento diferente al presentarse y descartarse durante una duración específica. clase abierta TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { /// Indicando si está presentando o descartando una transición. var presentando: Bool = verdadero /// Intervalo de tiempo en el que tiene lugar toda la transición. duración de alquiler privado: TimeInterval /// Inicializador predeterminado del animador de transición con valores predeterminados. /// - Duración del parámetro: Intervalo de tiempo en el que se produce toda la transición. /// - Parámetro presentando: Indicador si está presentando o descartando transición. public init(duración: TimeInterval = 0.5, presentando: Bool = true) { self.duration = duración self.presenting = presentando super.init() } /// Determina la duración de la transición. /// - Parámetro TransitionContext: Contexto de la transición actual. /// - Devoluciones: duración especificada en la inicialización del animador. public func TransitionDuration (utilizando TransitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { volver self.duration } /// Corazón del animador de transición, en esta función tiene lugar la transición. /// - Importante: anular esta función en un tipo de transición más concreto es crucial para realizar animaciones. /// - Parámetro TransitionContext: Contexto de transición. public func animateTransition (usando TransitionContext: UIViewControllerContextTransitioning) { } }
Basado en TransitionAnimator, creamos un archivo TransformTransition, cuya tarea será realizar una transición específica, transición con transformación (parenting)
/// Implementación de la transición de transformación. clase abierta TransformTransition: TransitionAnimator { /// Ver el modelo que contiene todas las especificaciones de transición necesarias. modelo de vista de var privada: TransformViewModel /// Inicializador predeterminado de transición de transformación. /// - Parámetro viewModel: Ver modelo de transición de transformación. /// - Duración del parámetro: Duración de la transición. init(viewModel: TransformViewModel, duración: TimeInterval) { self.viewModel = viewModel super.init(duración: duración, presentando: viewModel.presenting) }
La composición de la clase TransformTransition incluye TransformViewModel, que, como sugiere su nombre, informa el mecanismo de a qué modelos de vista se aplicará esta transición.
/// Ver el modelo de transición de transformación que contiene información básica al respecto. clase final TransformViewModel { /// Indica si la transición de transformación presenta o descarta la vista. Vamos a presentar: Bool /// Matriz de modelos con especificación sobre transformación para cada vista. dejar modelos: [TransformModel] /// Inicializador predeterminado del modelo de vista de transformación. /// - Presentación de parámetros: Indica si está presentando o descartando la transición de transformación. /// - Modelos de parámetros: matriz de modelos con especificación sobre la transformación para cada vista. init(presentando: Bool, modelos: [TransformModel]) { self.presenting = presentando self.modelos = modelos } }
El modelo de transformación es una clase auxiliar que describe los elementos específicos de las vistas involucradas en la transición ubicada en el padre, generalmente las vistas de un controlador que se pueden transformar.
En el caso de una transición, es un paso necesario porque esta transición consiste en las operaciones de vistas específicas entre estados dados.
Segundo: la implementación
Ampliamos el modelo de vista desde el que iniciamos la transición con Transformable, lo que nos obliga a implementar una función que preparará todos los elementos necesarios. El tamaño de esta función puede crecer muy rápidamente, por lo que le sugiero que la divida en partes más pequeñas, por ejemplo, por elemento.

/// Protocolo para la clase que quiere realizar la transición de transformación. protocolo Transformable: ViewModel { /// Prepara modelos de vistas que están involucradas en la transición. /// - Parámetro fromView: La vista desde la que comienza la transición /// - Parámetro toView: La vista a la que va la transición. /// - Parámetro presentando: Indica si está presentando o descartando. /// - Devoluciones: matriz de estructuras que contiene toda la información necesaria lista para transformar la transición para cada vista. func prepareTransitionModels(fromView: UIView, toView: UIView, presentando: Bool) -> [TransformModel] }
La suposición no es decir cómo buscar los datos de las vistas que participan en la transformación. En mi ejemplo, utilicé etiquetas que representan una vista dada. Tienes las manos libres en esta parte de la implementación.
Los modelos de transformaciones de vista específicas (TransformModel) son el modelo más pequeño de toda la lista. Consisten en información de transformación clave, como la vista de inicio, la vista de transición, el cuadro de inicio, el cuadro final, el centro de inicio, el centro final, las animaciones simultáneas y la operación final. No es necesario utilizar la mayoría de los parámetros durante la transformación, por lo que tienen sus propios valores predeterminados. Para resultados mínimos, es suficiente usar solo aquellos que se requieren.
/// Inicializador predeterminado del modelo de transformación con valores predeterminados. /// - Parámetro initialView: Vista desde la que se inicia la transición. /// - Parámetro phantomView: Vista que se presenta durante la transición de transformación. /// - Parámetro initialFrame: Marco de vista que inicia la transición de transformación. /// - Parámetro finalFrame: marco de vista que se presentará al final de la transición de transformación. /// - Parámetro initialCenter: Necesario cuando el punto de vista del centro inicial es diferente al centro de la vista inicial. /// - Parámetro finalCenter: Necesario cuando el punto de vista del centro final es diferente al centro de la vista final. /// - Parámetro paralelismoAnimación: Animación adicional de la vista realizada durante la transición de transformación. /// - Finalización de parámetros: bloque de código activado después de la transición de transformación. /// - Nota: solo se necesita la vista inicial para realizar la versión más minimalista de la transición de transformación. init(vistaInicial: UIView, Vista fantasma: UIView = UIView(), marcoInicial: CGRect = CGRect(), cuadro final: CGRect = CGRect(), centroinicial: CGPoint? = cero, Centro final: Punto CG? = cero, Animación paralela: (() -> Vacío)? = cero, finalización: (() -> Vacío)? = cero) { self.vistaInicial = VistaInicial self.phantomView = phantomView self.marcoinicial = marcoinicial self.finalFrame = finalFrame self.animaciónparalela = animaciónparalela self.completion = finalización self.initialCenter = initialCenter ?? CGPoint(x: marcoinicial.midX, y: marcoinicial.midY) self.finalCenter = finalCenter ?? PuntoCG(x: fotograma final.midX, y: fotogramafinal.midY) }
Es posible que su atención haya sido capturada por la vista fantasma. Este es el momento en el que explicaré el flujo de trabajo para las transiciones de iOS. En la forma más breve posible...

Cuando el usuario desea pasar a la siguiente escena, iOS prepara controladores específicos copiando los controladores de inicio (azul) y destino (verde) en la memoria. A continuación, se crea un contexto de transición a través del coordinador de transición que contiene el contenedor, una vista 'estúpida' que no contiene ninguna función especial, además de simular las vistas de transición entre las dos escenas.
El principio clave de trabajar con transiciones es no agregar ninguna vista real al contexto de transición, porque al final de la transición, se desasigna todo el contexto, junto con las vistas agregadas al contenedor. Estas son vistas que solo existen durante la transición y luego se eliminan.
Por lo tanto, el uso de vistas fantasma que son réplicas de vistas reales es una solución importante para esta transición.

En este caso, tenemos una transición que transforma una vista en otra cambiando su forma y tamaño. Para hacer esto, al comienzo de la transición, creo un PhantomView del elemento dado y lo agrego al contenedor. FadeView es una vista auxiliar para agregar suavidad a la transición general.
/// Corazón de la transición de transformación, donde se realiza la transición. Anular `TransitionAnimator.animateTransition(...)`. /// - Parámetro TransitionContext: El contexto de la transición de transformación actual. anula la función abierta animateTransition (utilizando TransitionContext: UIViewControllerContextTransitioning) { guard let toViewController = TransitionContext.view(forKey: .to), let fromViewController = TransitionContext.view(forKey: .from) else { devolver Log.unexpectedState() } let containerView = TransitionContext.containerView let duración = duración de la transición (usando: contexto de transición) let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue) dejar modelos = viewModel.models letpresentedView = presentando? aViewController : fromViewController modelos.forEach { $0.initialView.isHidden = true } vistapresentada.isHidden = true vistaContenedor.addSubview(toViewController) si presenta { containerView.insertSubview(fadeView, belowSubview: toViewController) } más { containerView.addSubview(fadeView) } containerView.addSubviews(viewModel.models.map { $0.phantomView })
En el siguiente paso, lo transformo en la forma de destino a través de transformaciones y, dependiendo de si es una presentación o un recuerdo, realiza operaciones adicionales para limpiar vistas específicas: esta es la receta completa para esta transición.
let animaciones: () -> Void = { [yo débil] en guard let self = self else { return Log.unexpectedState() } fadeView.alpha = self.presenting.cgFloatValue modelos.paraCada { let center = self.presenting ? $0.finalCenter : $0.initialCenter let transform = self.presenting ? $0.presentTransform : $0.dismissTransform $0.phantomView.setTransformAndCenter(transformar, centro) } modelos.compactMap { $0.parallelAnimation }.forEach { $0() } } dejar completar: (Bool) -> Void = { _ in TransitionContext.completeTransition(!transitionContext.transitionWasCancelled) vistapresentada.isHidden = false models.compactMap { $0.completion }.forEach { $0() } modelos.forEach { $0.initialView.isHidden = false } if !self.presenting && TransitionContext.transitionWasCancelled { aViewController.removeFromSuperview() fadeView.removeFromSuperview() } } UIView.animate(withDuration: duración, retraso: 0, usandoSpringWithDamping: 1, velocidad de resorte inicial: 0.5, opciones: .curveEaseOut, animaciones: animaciones, finalización: finalización)
Tercero: el ingrediente especial
Después de juntar todas las funciones, clases y protocolos, el resultado debería verse así:
El componente final de nuestra transición será su completa interactividad. Para este propósito usaremos un Pan Gesture agregado en la vista del controlador, TransitionInteractor…
/// Mediador para manejar la transición interactiva. clase final TransitionInteractor: UIPercentDrivenInteractiveTransition { /// Indica si la transición ha comenzado. var hasStarted = falso /// Indica si la transición debe terminar. var debeFinalizar = falso }
… que también inicializamos en el cuerpo del controlador.
/// Maneja el gesto panorámico en los elementos de la vista de colección y administra la transición. @objc func handlePanGesture(_ gestoRecognizer: UIPanGestureRecognizer) { Sea porcentaje Umbral: CGFloat = 0.1 dejar traducción = gestoRecognizer.translation(en: vista) let verticalMovement = traslación.y / vista.límites.altura let movimientohacia arriba = fminf(Flotante(movimientovertical), 0.0) let PorcentajeMovimientoArriba = fminf(abs(MovimientoAscendente), 0.9) let progreso = CGFloat(upwardMovementPercent) guard let interactor = interacciónController else {return} cambiar gestoRecognizer.estado { caso .comenzó: interactor.hasStarted = true dejar tapPosition = gestoRecognizer.ubicación (en: vista de colección) showDetailViewControllerFrom(ubicación: posición del toque) caso .cambiado: interactor.shouldFinish = progreso > umbral porcentual interactor.update(progreso) caso .cancelado: interactor.hasStarted = false interactor.cancel() caso .terminado: interactor.hasStarted = false interactor.debe terminar ? interactor.terminar() : interactor.cancel() defecto: descanso } }
La interacción lista debe ser la siguiente:
Si todo salió según lo planeado, nuestra aplicación ganará mucho más a los ojos de sus usuarios.

Descubierto sólo un pico de un iceberg
La próxima vez, explicaré la implementación de temas relacionados con el diseño de movimiento en las siguientes ediciones.
La aplicación, el diseño y el código fuente son propiedad de Miquido y fueron creados con pasión por diseñadores y programadores talentosos por cuyo uso no somos responsables en nuestras implementaciones. El código fuente detallado estará disponible en el futuro a través de nuestra cuenta de github. ¡Te invitamos a seguirnos!
¡Gracias por su atención y hasta pronto!