¿También te molestan las ventanas emergentes en las aplicaciones? En este artículo mostraré cómo ocultar interactivamente y mostrar ventanas emergentes, hacer que la animación sea interrumpible y no enfurecer a mis clientes.

En un artículo anterior, analicé cómo puedes animar la visualización de un nuevo controlador.
Nos decidimos por el hecho de que viewController
puede mostrar y esconderse animadamente:

Ahora le enseñaremos a responder al gesto de ocultamiento.
Transición Interactiva
Agrega un gesto cercano
Para enseñar al controlador a cerrar de forma interactiva, debe agregar un gesto y procesarlo. Todo el trabajo estará en la clase TransitionDriver
:
class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? }
Puede adjuntar un controlador en la ubicación de DimmPresentationController, dentro de PanelTransition:
private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController }
Al mismo tiempo, debe indicar que la piel se ha vuelto manejable (ya lo hicimos en el último artículo):
Manejar el gesto
Comencemos con el gesto de cierre: si arrastra el panel hacia abajo, comenzará la animación de cierre y el movimiento del dedo afectará el grado de cierre.
UIPercentDrivenInteractiveTransition
permite capturar la animación de transición y controlarla manualmente. Tiene métodos de update
, finish
y cancel
. Es conveniente realizar el procesamiento de gestos en su subclase.
Procesamiento de gestos
private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause()
.begin
Comience el despido de la manera más común. Guardamos el enlace al controlador en el método de link(to:)
.changed
Cuente el incremento y páselo al método de update
. El valor aceptado puede variar de 0 a 1, por lo que controlaremos el grado de finalización de la animación desde la interactionControllerForDismissal(using:)
método interactionControllerForDismissal(using:)
. Los cálculos se llevaron a cabo en la extensión del gesto, para que el código se vuelva más limpio.
Cálculos gestuales private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } }
Los cálculos se basan en maxTranslation
, lo calculamos como la altura del controlador que se muestra:
var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 }
.end
Nos fijamos en la integridad del gesto. Regla de finalización: si más de la mitad ha cambiado, cierre. En este caso, el desplazamiento debe ser considerado no solo por la coordenada actual, sino también por la velocity
. Entonces entendemos la intención del usuario: puede que no termine en el medio, pero deslice mucho hacia abajo. O viceversa: derribar, pero deslizar hacia arriba para volver.
Cálculos de ubicación proyectada private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat {
.cancelled
: sucederá si bloquea la pantalla del teléfono o si llaman. Puede manejarlo como un bloque .ended
o cancelar una acción.
.failed
: sucederá si otro gesto cancela el gesto. Entonces, por ejemplo, un gesto de arrastre puede cancelar un gesto de toque.
.possible
: el estado inicial del gesto, generalmente no requiere mucho trabajo.
Ahora el panel también se puede cerrar con un deslizamiento, pero el botón de dismiss
se ha dismiss
. Esto sucedió porque hay una propiedad wantsInteractiveStart
en TransitionDriver
, por defecto es true
. Esto es normal para un deslizamiento, pero bloquea el dismiss
habitual.
Analicemos el comportamiento en función del estado del gesto. Si el gesto comenzó, entonces este es un cierre interactivo, y si no comenzó, entonces el habitual:
override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } }
Ahora el usuario puede controlar el hide:

Transición de interrupción
Supongamos que comenzamos a cerrar nuestra tarjeta, pero cambiamos de opinión y queremos volver. Es simple: en un estado .began
, llamamos a pause()
para detener.
Pero necesita separar dos escenarios:
- cuando empezamos a escondernos del gesto;
- cuando interrumpimos el actual.
Para hacer esto, después de detenerse, marque percentComplete:
si es 0, entonces comenzamos a cerrar la tarjeta manualmente, además tenemos que llamar a dismiss
. Si no es 0, entonces la ocultación ya ha comenzado, es suficiente para detener la animación:
case .began: pause()
Presiono el botón e inmediatamente deslizo hacia arriba para cancelar el ocultamiento:

Dejar de mostrar el controlador
La situación inversa: la tarjeta comenzó a aparecer, pero no la necesitamos. Lo atrapamos y lo enviamos deslizando hacia abajo. Puede interrumpir la animación de la pantalla del controlador en los mismos pasos.
Devuelva el controlador como un controlador de pantalla interactivo:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver }
Procese el gesto, pero con valores de sesgo inverso e integridad:
private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } }
Para separar mostrar y ocultar, ingresé enum con la dirección de animación actual:
enum TransitionDirection { case present, dismiss }
La propiedad se almacena en TransitionDriver
y afecta qué gestor de gestos se utilizará:
var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } }
También afecta a wantsInteractiveStart
. No planeamos mostrar el controlador con un gesto, por lo que devolvemos false
para .present
:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } }
Bueno, queda por cambiar la dirección del gesto cuando el controlador se mostró completamente. El mejor lugar está en PresentationController
:
override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } }
¿Es posible sin enumeración?Parece que podemos confiar en las propiedades del controlador isBeingPresented
y isBeingDismissed
. Pero solo muestran el proceso, y también necesitamos posibles direcciones: al comienzo del cierre interactivo, ambos valores serán false
, y ya necesitamos saber que esta es la dirección del cierre. Esto se puede resolver mediante condiciones adicionales para verificar la jerarquía de los controladores, pero la asignación explícita a través de enum
parece ser una solución más simple.
Ahora puedes interrumpir la animación del espectáculo. Presiono el botón e inmediatamente deslizo hacia abajo:

Mostrar por gesto
Si está creando un menú de hamburguesas para una aplicación, lo más probable es que desee mostrarlo con un gesto. Esto funciona igual que la ocultación interactiva, pero en un gesto, en lugar de dismiss
llamamos present
.
Comencemos desde el final. En handlePresentation(recognizer:)
muestra el controlador:
case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) }
Vamos a mostrar interactivamente:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … }
Para que el código funcione, no hay suficientes enlaces para presentingController
y presentedController
. Los pasaremos al crear el gesto, agregaremos el UIScreenEdgePanGestureRecognizer
:
func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController
Puede transferir controladores al crear PanelTransition
:
class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() }
Queda por crear la PanelTransition
:
viewDidLoad
un controlador child
en viewDidLoad
, ya que es posible que necesitemos un controlador en cualquier momento.- Crear
PanelTransition
. En su constructor, el gesto está vinculado al controlador. - Ponga el Delegado de transición para el controlador secundario.
Para fines de capacitación, deslizo desde abajo, pero esto entra en conflicto con el cierre de la aplicación en el iPhone X y el centro de control. El uso de preferredScreenEdgesDeferringSystemGestures
deshabilitó el deslizamiento del sistema desde abajo.
class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController()
Después del cambio, resultó que había un problema: después del primer cierre del panel, permanece para siempre en el estado de TransitionDirection.dismiss
. Establezca el estado correcto después de ocultar el controlador en PresentationController
:
override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } }
El código de visualización interactivo se puede ver en un hilo separado . Se ve así:

Conclusión
Como resultado, podemos mostrar el controlador con animación interrumpida, y el usuario tiene control sobre lo que está sucediendo en la pantalla. Esto es mucho mejor, porque la animación ya no bloquea la interfaz, puede cancelarse o incluso acelerarse.
Un ejemplo se puede ver en github.
Suscríbase al canal móvil de Dodo Pizza.