Controladores de vista modal: cómo mostrar y descartar

82

Me estoy rompiendo la cabeza durante la última semana sobre cómo resolver el problema de mostrar y descartar varios controladores de vista. Creé un proyecto de muestra y pegué el código directamente desde el proyecto. Tengo 3 controladores de vista con sus correspondientes archivos .xib. MainViewController, VC1 y VC2. Tengo dos botones en el controlador de vista principal.

- (IBAction)VC1Pressed:(UIButton *)sender
{
    VC1 *vc1 = [[VC1 alloc] initWithNibName:@"VC1" bundle:nil];
    [vc1 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc1 animated:YES completion:nil];
}

Esto abre VC1 sin problemas. En VC1, tengo otro botón que debería abrir VC2 y al mismo tiempo descartar VC1.

- (IBAction)buttonPressedFromVC1:(UIButton *)sender
{
    VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];
    [vc2 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc2 animated:YES completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
} // This shows a warning: Attempt to dismiss from view controller <VC1: 0x715e460> while a presentation or dismiss is in progress!


- (IBAction)buttonPressedFromVC2:(UIButton *)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
} // This is going back to VC1. 

Quiero que vuelva al controlador de vista principal y, al mismo tiempo, VC1 debería haberse eliminado de la memoria para siempre. VC1 solo debería aparecer cuando hago clic en el botón VC1 en el controlador principal.

El otro botón en el controlador de vista principal también debería poder mostrar VC2 directamente sin pasar por VC1 y debería volver al controlador principal cuando se hace clic en un botón en VC2. No hay ningún código de ejecución prolongada, bucles ni temporizadores. Solo llamadas básicas para ver los controladores.

Hema
fuente

Respuestas:

189

Esta línea:

[self dismissViewControllerAnimated:YES completion:nil];

no se está enviando un mensaje a sí mismo, en realidad está enviando un mensaje a su VC presentador, pidiéndole que haga el rechazo. Cuando presenta un VC, crea una relación entre el VC que presenta y el presentado. Por lo tanto, no debe destruir el VC que se presenta mientras se está presentando (el VC presentado no puede devolver ese mensaje de descarte ...). Como realmente no lo está tomando en cuenta, está dejando la aplicación en un estado confuso. Vea mi respuesta Descartar un controlador de vista presentado en el que recomiendo que este método esté escrito más claramente:

[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

En su caso, debe asegurarse de que todo el control se realice en formato mainVC . Debe usar un delegado para enviar el mensaje correcto a MainViewController desde ViewController1, para que mainVC pueda descartar VC1 y luego presentar VC2.

En VC2 VC1 agregue un protocolo en su archivo .h arriba de @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

y más abajo en el mismo archivo en la sección @interface declare una propiedad para contener el puntero delegado:

@property (nonatomic,weak) id <ViewController1Protocol> delegate;

En el archivo .m VC1, el método del botón de descarte debe llamar al método delegado

- (IBAction)buttonPressedFromVC1:(UIButton *)sender {
    [self.delegate dissmissAndPresentVC2]
}

Ahora en mainVC, configúrelo como delegado de VC1 al crear VC1:

- (IBAction)present1:(id)sender {
    ViewController1* vc = [[ViewController1 alloc] initWithNibName:@"ViewController1" bundle:nil];
    vc.delegate = self;
    [self present:vc];
}

e implementar el método delegado:

- (void)dismissAndPresent2 {
    [self dismissViewControllerAnimated:NO completion:^{
        [self present2:nil];
    }];
}

present2:puede ser el mismo método que el VC2Pressed:método IBAction del botón. Tenga en cuenta que se llama desde el bloque de finalización para garantizar que VC2 no se presente hasta que VC1 se descarte por completo.

Ahora se está moviendo de VC1-> VCMain-> VC2, por lo que probablemente querrá que solo una de las transiciones esté animada.

actualizar

En sus comentarios expresa sorpresa por la complejidad necesaria para lograr algo aparentemente simple. Le aseguro que este patrón de delegación es tan fundamental para gran parte de Objective-C y Cocoa, y este ejemplo es el más simple que puede obtener, que realmente debería hacer el esfuerzo para sentirse cómodo con él.

En la Guía de programación del controlador de vista de Apple, tienen esto que decir :

Descartar un controlador de vista presentado

Cuando llega el momento de descartar un controlador de vista presentado, el enfoque preferido es dejar que el controlador de vista de presentación lo descarte. En otras palabras, siempre que sea posible, el mismo controlador de vista que presentó el controlador de vista también debe asumir la responsabilidad de descartarlo. Aunque existen varias técnicas para notificar al controlador de vista de presentación que su controlador de vista presentada debe descartarse, la técnica preferida es la delegación. Para obtener más información, consulte "Uso de la delegación para comunicarse con otros controladores".

Si realmente piensa en lo que quiere lograr y cómo lo está haciendo, se dará cuenta de que enviar mensajes a su MainViewController para que haga todo el trabajo es la única salida lógica dado que no desea utilizar un NavigationController. Si se hace uso de un NavController, en efecto, son 'delegar', aunque no de forma explícita, a la NavController que hacer todo el trabajo. Debe haber algún objeto que mantenga un seguimiento central de lo que sucede con la navegación de su VC, y necesita algún método para comunicarse con él, hagas lo que hagas.

En la práctica, el consejo de Apple es un poco extremo ... en casos normales, no es necesario crear un delegado y un método dedicados, puede confiar en [self presentingViewController] dismissViewControllerAnimated: ; es cuando, en casos como el suyo, desea que su despido tenga otros efectos en el control remoto. objetos que debes cuidar.

Aquí hay algo que podría imaginar para trabajar sin todas las molestias de los delegados ...

- (IBAction)dismiss:(id)sender {
    [[self presentingViewController] dismissViewControllerAnimated:YES 
                                                        completion:^{
        [self.presentingViewController performSelector:@selector(presentVC2:) 
                                            withObject:nil];
    }];

}

Después de pedirle al controlador de presentación que nos descarte, tenemos un bloque de finalización que llama a un método en el controlador de presentación de visualización para invocar VC2. No se necesita delegado. (Un gran punto de venta de los bloques es que reducen la necesidad de delegados en estas circunstancias). Sin embargo, en este caso hay algunas cosas que se interponen en el camino ...

  • en VC1 no sabes que mainVC implementa el métodopresent2 ; puede terminar con errores o fallas difíciles de depurar. Los delegados le ayudan a evitar esto.
  • una vez que se descarta VC1, no está realmente disponible para ejecutar el bloque de finalización ... ¿o no? ¿Self.presentingViewController significa algo más? No sabes (yo tampoco) ... con un delegado, no tienes esta incertidumbre.
  • Cuando intento ejecutar este método, simplemente se bloquea sin advertencias ni errores.

Así que por favor ... ¡tómate el tiempo para aprender a delegar!

actualización2

En su comentario, ha logrado que funcione usando esto en el controlador de botón de descarte de VC2:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 

Sin duda, esto es mucho más simple, pero te deja con una serie de problemas.

Acoplamiento
estrecho Está conectando la estructura de su viewController. Por ejemplo, si insertara un nuevo viewController antes de mainVC, su comportamiento requerido se interrumpiría (navegaría al anterior). En VC1 también ha tenido que #importar VC2. Por lo tanto, tiene bastantes interdependencias, lo que rompe los objetivos de OOP / MVC.

Al usar delegados, ni VC1 ni VC2 necesitan saber nada sobre mainVC o sus antecedentes, por lo que mantenemos todo acoplado libremente y modular.

La memoria
VC1 no se ha ido, todavía tiene dos indicadores:

  • presentedViewControllerpropiedad de mainVC
  • presentingViewControllerPropiedad de VC2

Puede probar esto iniciando sesión, y también simplemente haciendo esto desde VC2

[self dismissViewControllerAnimated:YES completion:nil]; 

Todavía funciona, todavía te lleva de vuelta a VC1.

Eso me parece una pérdida de memoria.

La pista de esto está en la advertencia que está recibiendo aquí:

[self presentViewController:vc2 animated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
 // Attempt to dismiss from view controller <VC1: 0x715e460>
 // while a presentation or dismiss is in progress!

La lógica se rompe, ya que intenta descartar el VC de presentación del cual VC2 es el VC presentado. El segundo mensaje no se ejecuta realmente; bueno, tal vez sucedan algunas cosas, pero aún te quedan dos punteros a un objeto del que pensabas que te habías deshecho. ( editar - He comprobado esto y no es tan malo, ambos objetos desaparecen cuando regresas a mainVC )

Esa es una forma bastante prolija de decir: por favor, use delegados. Si ayuda, hice otra breve descripción del patrón aquí:
¿Pasar un controlador en un constructor siempre es una mala práctica?

actualización 3
Si realmente desea evitar a los delegados, esta podría ser la mejor salida:

En VC1:

[self presentViewController:VC2
                   animated:YES
                 completion:nil];

Pero no descarte nada ... como comprobamos, en realidad no sucede de todos modos.

En VC2:

[self.presentingViewController.presentingViewController 
    dismissViewControllerAnimated:YES
                       completion:nil];

Como (sabemos) no hemos descartado VC1, podemos volver a través de VC1 a MainVC. MainVC descarta VC1. Debido a que VC1 se ha ido, se presenta que VC2 lo acompaña, por lo que está de regreso en MainVC en un estado limpio.

Todavía está muy acoplado, ya que VC1 necesita saber sobre VC2, y VC2 necesita saber que se llegó a través de MainVC-> VC1, pero es lo mejor que obtendrá sin un poco de delegación explícita.

fundición
fuente
1
parece complicado. Traté de seguir y copiar al punto pero me perdí en el medio. ¿Hay alguna otra forma de conseguirlo ?. También quería agregar que en el delegado de la aplicación, el controlador principal está configurado como el controlador de vista raíz. No quiero usar controladores de navegación, pero me pregunto por qué debería ser tan complicado de lograr. Para resumir, cuando se inicia la aplicación, muestro un controlador de vista principal con 2 botones. Al hacer clic en el primer botón, se carga VC1. Hay un botón en VC1 y al hacer clic en él debería cargar VC2 sin errores ni advertencias y al mismo tiempo descartar VC1 de la memoria.
Hema
En VC2, tengo un botón y al hacer clic en él, debería descartar VC2 de la memoria y el control debería volver al controlador principal y no a VC1.
Hema
@Hema, he entendido perfectamente tus requisitos y te aseguro que esta es la forma correcta de hacerlo. Actualicé mi respuesta con un poco más de información, espero que ayude. Si probó mi enfoque y se quedó atascado, plantee una nueva pregunta que muestre exactamente lo que no funciona para que podamos ayudarlo. También puede vincular a esta pregunta para mayor claridad.
fundición
Hola, lo fue: Gracias por tu comprensión. También estoy hablando de otro hilo (hilo original) y acabo de publicar un fragmento de las sugerencias mencionadas allí. Estoy probando todas las respuestas de expertos para concretar este problema. La URL está aquí: stackoverflow.com/questions/14840318/…
Hema
1
@Honey - Quizás sea así, pero la declaración fue una respuesta retórica a un fragmento de pseudocódigo 'imaginado'. El punto que quería hacer no se trata de retener las trampas de los ciclos, sino de educar al interrogador sobre por qué la delegación es un patrón de diseño valioso (que, por cierto, evita ese problema). Creo que ese es el argumento engañoso aquí: la pregunta es sobre VC modales, pero el valor de la respuesta radica principalmente en su explicación del patrón de delegado, utilizando la pregunta, y las evidentes frustraciones del OP, como catalizador. ¡Gracias por su interés (y sus ediciones)!
fundición
12

Ejemplo en Swift , que muestra la explicación de la fundición anterior y la documentación de Apple:

  1. Basándose en la documentación de Apple y la explicación de la fundición anterior (corrigiendo algunos errores), presente la versión de ViewController usando el patrón de diseño delegado:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func dismissViewController1AndPresentViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        vc1.modalTransitionStyle = UIModalTransitionStyle.FlipHorizontal
        self.presentViewController(vc1, animated: true, completion: nil)
    }

    func dismissViewController1AndPresentViewController2() {
        self.dismissViewControllerAnimated(false, completion: { () -> Void in
            let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
            self.presentViewController(vc2, animated: true, completion: nil)
        })
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.dismissViewController1AndPresentViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
  1. Basándose en la explicación de la fundición anterior (corrigiendo algunos errores), la versión pushViewController usando el patrón de diseño delegado:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func popViewController1AndPushViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        self.navigationController?.pushViewController(vc1, animated: true)
    }

    func popViewController1AndPushViewController2() {
        self.navigationController?.popViewControllerAnimated(false)
        let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
        self.navigationController?.pushViewController(vc2, animated: true)
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.popViewController1AndPushViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
Rey Mago
fuente
en su ViewControllerclase de ejemplo es mainVC, ¿verdad?
Miel
10

Creo que no entendiste algunos conceptos básicos sobre los controladores de vista modal de iOS. Cuando descarta VC1, los controladores de vista presentados por VC1 también se descartan. Apple pretendía que los controladores de vista modal fluyeran de manera apilada; en su caso, VC2 es presentado por VC1. Está descartando VC1 tan pronto como presente VC2 de VC1, por lo que es un desastre total. Para lograr lo que desea, buttonPressedFromVC1 debe tener el mainVC presente VC2 inmediatamente después de que VC1 se descarte. Y creo que esto se puede lograr sin delegados. Algo parecido a esto:

UIViewController presentingVC = [self presentingViewController];
[self dismissViewControllerAnimated:YES completion:
 ^{
    [presentingVC presentViewController:vc2 animated:YES completion:nil];
 }];

Tenga en cuenta que self.presentingViewController se almacena en alguna otra variable, porque después de que vc1 se descarta, no debe hacer ninguna referencia a él.

Radu Simionescu
fuente
1
¡tan sencillo! Me gustaría que otros se desplazaran hacia abajo hasta su respuesta en lugar de detenerse en la publicación superior.
Ryan Loggerythm
en el código del OP, ¿por qué no [self dismiss...]sucede después de [self present...] que finaliza? No es que esté sucediendo algo asincrónico
Cariño
1
@Honey en realidad, ocurre algo asincrónico al llamar a presentViewController, es por eso que tiene un controlador de finalización. Pero incluso usando eso, si descarta el controlador de vista de presentación después de que presenta algo, todo lo que presenta también se descarta. Entonces, OP realmente quiere presentar el controlador de vista de otro presentador en realidad, para que pueda descartar el actual
Radu Simionescu
Pero incluso usando eso, si descarta el controlador de vista de presentación después de que presenta algo, todo lo que presenta se descarta también ... Ajá, entonces el compilador básicamente dice "lo que estás haciendo es estúpido. Acabas de deshacer tu anterior línea de código (como VC1 me descartaré a mí mismo y lo que sea que esté presentando). No lo hagas "¿verdad?
Cariño
El compilador no "dirá" nada al respecto, y también podría ser el caso de que no se bloquee al ejecutar esto, solo que se comportará de una manera que el programador no espera
Radu Simionescu
5

Radu Simionescu - ¡trabajo increíble! y a continuación Su solución para los amantes de Swift:

@IBAction func showSecondControlerAndCloseCurrentOne(sender: UIButton) {
    let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("ConrollerStoryboardID") as UIViewControllerClass // change it as You need it
    var presentingVC = self.presentingViewController
    self.dismissViewControllerAnimated(false, completion: { () -> Void   in
        presentingVC!.presentViewController(secondViewController, animated: true, completion: nil)
    })
}
chrisco
fuente
esto de alguna manera me frustra que realmente funcione. No entiendo por qué el bloque no captura "self.presentingViewController" y se necesita una referencia fuerte, es decir, "var presentVC" .. de todos modos, esto funciona. thx
emdog4
1

Yo quería esto:

MapVC es un mapa en pantalla completa.

Cuando presiono un botón, se abre PopupVC (no en pantalla completa) sobre el mapa.

Cuando presiono un botón en PopupVC, vuelve a MapVC y luego quiero ejecutar viewDidAppear.

Hice esto:

MapVC.m: en la acción del botón, un segue programáticamente y establece delegado

- (void) buttonMapAction{
   PopupVC *popvc = [self.storyboard instantiateViewControllerWithIdentifier:@"popup"];
   popvc.delegate = self;
   [self presentViewController:popvc animated:YES completion:nil];
}

- (void)dismissAndPresentMap {
  [self dismissViewControllerAnimated:NO completion:^{
    NSLog(@"dismissAndPresentMap");
    //When returns of the other view I call viewDidAppear but you can call to other functions
    [self viewDidAppear:YES];
  }];
}

PopupVC.h: antes de @interface, agregue el protocolo

@protocol PopupVCProtocol <NSObject>
- (void)dismissAndPresentMap;
@end

después de @interface, una nueva propiedad

@property (nonatomic,weak) id <PopupVCProtocol> delegate;

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}
Mer
fuente
1

Resolví el problema usando UINavigationController al presentar. En MainVC, al presentar VC1

let vc1 = VC1()
let navigationVC = UINavigationController(rootViewController: vc1)
self.present(navigationVC, animated: true, completion: nil)

En VC1, cuando me gustaría mostrar VC2 y descartar VC1 al mismo tiempo (solo una animación), puedo tener una animación de empuje por

let vc2 = VC2()
self.navigationController?.setViewControllers([vc2], animated: true)

Y en VC2, al cerrar el controlador de vista, como de costumbre podemos usar:

self.dismiss(animated: true, completion: nil)
Duong Ngo
fuente