Detectar cuando se descarta un controlador de vista presentado

81

Digamos que tengo una instancia de una clase de controlador de vista llamada VC2. En VC2, hay un botón "cancelar" que se cerrará. Pero no puedo detectar ni recibir ninguna devolución de llamada cuando el botón "cancelar" se activó. VC2 es una caja negra.

Un controlador de vista (llamado VC1) presentará VC2 usando el presentViewController:animated:completion:método.

¿Qué opciones tiene VC1 para detectar cuándo se descartó VC2?

Editar: desde el comentario de @rory mckinnel y la respuesta de @NicolasMiari, probé lo siguiente:

En VC2:

-(void)cancelButton:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:^{

    }];
//    [super dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

En VC1:

//-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
- (void)dismissViewControllerAnimated:(BOOL)flag
                           completion:(void (^ _Nullable)(void))completion
{
    NSLog(@"%s ", __PRETTY_FUNCTION__);
    [super dismissViewControllerAnimated:flag completion:completion];
//    [self dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

Pero el dismissViewControllerAnimateden el VC1 no fue llamado.

usuario523234
fuente
1
en VC1 se llamará al método viewWillAppear
Istvan
1
Según los documentos, el controlador presentador es responsable del despido real. Cuando el controlador presentado se descarta, le pedirá al presentador que lo haga por él. Entonces, si anula dismissViewControllerAnimateden su controlador VC1, creo que se llamará cuando presione cancelar en VC2. Detecte el descarte y luego llame a la versión de superclases que hará el descarte real.
Rory McKinnel
1
Puede probar su anulación llamando [self.presentingViewController dismissViewControllerAnimated]. Puede ser que el código interno tenga un mecanismo diferente para pedirle al presentador que haga el descarte.
Rory McKinnel
@RoryMcKinnel: El uso de self.presentingViewController funcionó en mi laboratorio VC2, así como en la caja negra real. Si pone sus comentarios en la respuesta, lo seleccionaré como la respuesta. Gracias.
user523234
Se puede encontrar una solución para esto en esta publicación relacionada: stackoverflow.com/a/34571641/3643020
Campbell_Souped

Respuestas:

64

Según los documentos, el controlador presentador es responsable del despido real. Cuando el controlador presentado se descarta, le pedirá al presentador que lo haga por él. Entonces, si anula descartarViewControllerAnimated en su controlador VC1, creo que se llamará cuando presione cancelar en VC2. Detecte el descarte y luego llame a la versión de superclases que hará el descarte real.

Como se encontró en la discusión, esto no parece funcionar. En lugar de confiar en el mecanismo subyacente, en lugar de llamar dismissViewControllerAnimated:completiona sí mismo VC2, llamada dismissViewControllerAnimated:completionen self.presentingViewControlleren VC2. Esto luego llamará a su anulación directamente.

Un enfoque mejor en conjunto sería hacer que VC2 proporcione un bloque que se llama cuando se completa el controlador modal.

Entonces, en VC2, proporcione una propiedad de bloque, por ejemplo, con el nombre onDoneBlock.

En VC1 presentas lo siguiente:

  • En VC1, cree VC2

  • Establezca el controlador hecho para VC2 como: VC2.onDoneBlock={[VC2 dismissViewControllerAnimated:YES completion:nil]};

  • Presente el controlador VC2 como normal usando [self presentViewController: VC2 animado: SÍ completado: nulo];

  • En VC2, en la llamada de acción de cancelación de destino self.onDoneBlock();

El resultado es que VC2 le dice a quien lo plantea que está hecho. Puede extender el onDoneBlockpara tener argumentos que indiquen si el modal se completó, canceló, tuvo éxito, etc.

Rory McKinnel
fuente
2
Solo quiero agradecer y apreciar lo bien que funciona esto ... ¡incluso después de 4 años! ¡Gracias!
Anna
45

Hay una propiedad booleana especial dentro UIViewControllerllamada isBeingDismissedque puede usar para este propósito:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if isBeingDismissed {
        // TODO: Do your stuff here.
    }
}
Joris Weimar
fuente
3
La mejor respuesta más fácil, aborda correctamente la mayoría de los problemas y no necesita implementaciones adicionales.
reafirmado el
No funciona correctamente sin emparejarse con viewDidAppear.
Dmitry
9
En una presentación modal de iOS13, esto será cierto cuando un usuario comience a arrastrar el controlador para descartar, pero puede optar por no completar el descarte.
Estel
44

Usar una propiedad de bloque

Declarar en VC2

var onDoneBlock : ((Bool) -> Void)?

Configuración en VC1

VC2.onDoneBlock = { result in
                // Do something
            }

Llame a VC2 cuando esté a punto de despedir

onDoneBlock!(true)
Brycejl
fuente
@ Bryce64 No me está funcionando, obtuve "Subproceso 1: Error fatal: inesperadamente encontrado nulo al desenvolver un valor opcional", en el punto donde el código va a onDoneBlock! (Verdadero)
Lucas
@Lucas Parece que no lo declaró correctamente en VC1. Los "!" obliga al desenvuelto a forzar un error si no lo ha configurado correctamente.
brycejl
1
Supone que solo se presenta un controlador de vista. Podrías estar en una pila de navegación Dios sabe dónde.
Lee Probert
@LeeProbert Exactamente. Tenemos un controlador de navegación presentado con aproximadamente 10 posibles controladores secundarios en su lado de su pila, y casi todos pueden desencadenar el despido ... en esta situación, cualquier bloque de finalización tendría que pasarse a los 10 controladores de este tipo
Igor Vasilev
13

Tanto el controlador de vista presentado como el presentado pueden llamar dismissViewController:animated:para descartar el controlador de vista presentado.

La primera opción es (posiblemente) la "correcta", en cuanto al diseño: el mismo controlador de vista "principal" es responsable de presentar y descartar el controlador de vista modal ("secundario").

Sin embargo, lo último es más conveniente: normalmente, el botón "descartar" se adjunta a la vista del controlador de vista presentado, y tiene dicho controlador de vista configurado como su objetivo de acción.

Si está adoptando el enfoque anterior, ya conoce la línea de código en su controlador de vista de presentación donde ocurre el despido: ejecute su código justo después dismissViewControllerAnimated:completion:o dentro del bloque de finalización.

Si está adoptando el último enfoque (el controlador de vista presentado se descarta a sí mismo), tenga en cuenta que llamar dismissViewControllerAnimated:completion:desde el controlador de vista presentado hace que UIKit a su vez llame a ese método en el controlador de vista de presentación:

Discusión

El controlador de vista de presentación es responsable de descartar el controlador de vista que presentó. Si llama a este método en el controlador de vista presentado, UIKit le pide al controlador de vista de presentación que maneje el despido.

( fuente: Referencia de clase UIViewController )

Entonces, para interceptar dicho evento, puede anular ese método en el controlador de vista de presentación :

override func dismiss(animated flag: Bool,
                         completion: (() -> Void)?) {
    super.dismiss(animated: flag, completion: completion)

    // Your custom code here...
}
Nicolas Miari
fuente
1
No hay problema. Pero resulta que no funciona como se esperaba. Afortunadamente, la respuesta de @ RoryMcKinnel parece dar más opciones.
Nicolas Miari
Aunque este enfoque es lo suficientemente genérico como para subclasificar el controlador de vista de un anuncio de controlador de vista base que anula descartarViewControllerAnimated en eso. Pero falla si intenta terminar en un controlador de vista en el controlador de vista de navegación
hariszaman
4
¡No se llama cuando el usuario cierra el controlador de vista modal con un deslizamiento desde la parte superior!
Dmitry
3
extension Foo: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        //call whatever you want
    }
}

vc.presentationController?.delegate = foo
Iván
fuente
1
iOS 13.0+solo
gondo
2

Puede utilizar el segue de desenrollado para realizar esta tarea, sin necesidad de utilizar el controlador de descarteModalViewController. Defina un método de segue de desenrollado en su VC1.

Vea este enlace sobre cómo crear la secuencia de relajación, https://stackoverflow.com/a/15839298/5647055 .

Suponiendo que la transición de desconexión esté configurada, en el método de acción definido para el botón "Cancelar", puede realizar la transición como:

[self performSegueWithIdentifier:@"YourUnwindSegueName" sender:nil];

Ahora, siempre que presione el botón "Cancelar" en el VC2, se cerrará y aparecerá VC1. También llamará al método de desenrollado, definido en VC1. Ahora, ya sabe cuándo se descarta el controlador de vista presentado.

valar Morghulis
fuente
2

Utilizo lo siguiente para indicar a un coordinador que el controlador de vista está "listo". Esto se usa en una AVPlayerViewControllersubclase en una aplicación tvOS y se llamará después de que se haya completado la transición de despido de playerVC:

class PlayerViewController: AVPlayerViewController {
  var onDismissal: (() -> Void)?

  override func beginAppearanceTransition(_ isAppearing: Bool, animated: Bool) {
    super.beginAppearanceTransition(isAppearing, animated: animated)
    transitionCoordinator?.animate(alongsideTransition: nil,
      completion: { [weak self] _ in
         if !isAppearing {
            self?.onDismissal?()
        }
    })
  }
}
codificador de frutas
fuente
No debe heredar de AVPLayerViewController. Apple docs dice: "No se admite la subclasificación de AVPlayerViewController y la invalidación de sus métodos, lo que da como resultado un comportamiento indefinido".
Neru
2

Usar el willMove(toParent: UIViewController?)de la siguiente manera pareció funcionar para mí. (Probado en iOS12).

override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent);

    if parent == nil
    {
        // View controller is being removed.
        // Perform onDismiss action
    }
}
Shavi
fuente
1

@ user523234 - "Pero no se llamaba al DispenssViewControllerAnimated del VC1"

No puede suponer que VC1 realmente hace la presentación; podría ser el controlador de vista raíz, VC0, por ejemplo. Hay 3 controladores de vista involucrados:

  • sourceViewController
  • presentViewController
  • PresentViewController

En su ejemplo, VC1 = sourceViewController, VC2 = presentedViewController, ?? = presentingViewController- tal vez VC1, tal vez no.

Sin embargo, siempre puede confiar en que se llame a VC1.animationControllerForDismissedController (si ha implementado los métodos delegados) al descartar VC2 y en ese método puede hacer lo que quiera con VC1

Andrew Coad
fuente
1

He visto esta publicación tantas veces al tratar este problema, que pensé que finalmente podría arrojar algo de luz sobre una posible respuesta.

Si lo que necesita es saber si las acciones iniciadas por el usuario (como los gestos en la pantalla) provocaron el rechazo de un UIActionController , y no quiere invertir tiempo en crear subclases o extensiones o lo que sea en su código, existe una alternativa.

Resulta que la propiedad popoverPresentationController de un UIActionController (o, más bien, cualquier UIViewController a tal efecto), tiene un delegado que puede establecer en cualquier momento en su código, que es de tipo UIPopoverPresentationControllerDelegate , y tiene los siguientes métodos:

Asigne el delegado de su controlador de acción, implemente su método (s) de elección en la clase de delegado (vista, controlador de vista o lo que sea), ¡y listo!

Espero que esto ayude.

Izhido
fuente
Y esos están en desuso desde iOS 13. Doh
pronebird
0
  1. Cree un archivo de clase (.h / .m) y asígnele el nombre: DismissSegue
  2. Seleccione una subclase de: UIStoryboardSegue

  3. Vaya al archivo DismissSegue.m y escriba el siguiente código:

    - (void)perform {
        UIViewController *sourceViewController = self.sourceViewController;
        [sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
    
  4. Abra el guión gráfico y luego Ctrl + arrastre desde el botón cancelar a VC1 y seleccione Acción Segue como Descartar y listo.

Tapansinh Solanki
fuente
0

Si anula la atenuación del controlador de vista:

override func removeFromParentViewController() {
    super.removeFromParentViewController()
    // your code here
}

Al menos esto funcionó para mí.

mxcl
fuente
@JohnScalo no es cierto, bastantes de las jerarquías de controladores de vista "nativas" se implementan con las primitivas hijo / padre.
mxcl
0

overrideing me viewDidAppearhizo el truco. Usé un Singleton en mi modal y ahora puedo configurar y obtener de eso dentro del VC que llama, el modal y en cualquier otro lugar.

A
fuente
viewDidAppear?
Dmitry
1
Quieres decir viewDidDisappear?
Dale
0

viewWillDisappearFunción de anulación en el controlador de vista presentado.

override func viewWillDisappear(_ animated: Bool) {
    //Your code here
}
Priwiljay
fuente
No funciona correctamente sin emparejarse con viewDidAppear.
Dmitry
1
Asegúrese de llamar a super.viewWillDisappear
Dale
0

Como se ha mencionado, la solución es utilizar override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).

Para aquellos que se preguntan por qué override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil)no siempre parece funcionar, es posible que descubran que la llamada está siendo interceptada por un UINavigationControllersi está siendo administrada. Escribí una subclase que debería ayudar:

class DismissingNavigationController: UINavigationController { override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { super.dismiss(animated: flag, completion: completion) topViewController?.dismiss(animated: flag, completion: completion) } }

Steve
fuente
0

Si desea manejar el cierre del controlador de vista, debe usar el código a continuación.

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.isBeingDismissed && self.completion != NULL) {
        self.completion();
    }
}

Desafortunadamente, no podemos llamar a la finalización en el método anulado, (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion;porque este método se llama solo si llama al método de descarte de este controlador de vista.

Arthur Kvaratskhelija
fuente
Pero viewWillDisappeartampoco funciona correctamente sin emparejarse con viewDidAppear.
Dmitry
viewWillDisappear se llama cuando el VC está completamente cubierto (por ejemplo, con un modal). Es posible que no haya sido despedido
Lou Franco
0

Otra opción es escuchar dispatsalTransitionDidEnd () de su UIPresentationController personalizado

Igor Vasilev
fuente
0

He usado deinit para ViewController

deinit {
    dataSource.stopUpdates()
}

Se llama a un desinicializador inmediatamente antes de que se desasigne una instancia de clase.

Dan Alboteanu
fuente