Controladores de vista modal: cómo mostrar y descartar

Me estoy rompiendo la cabeza la última semana sobre cómo resolver el problema mostrando y descartando múltiples controladores de visualización. 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 mientras que al mismo tiempo descarta 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  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 regrese al controlador de vista principal mientras que al mismo tiempo VC1 debería haber sido eliminado de la memoria para siempre. VC1 solo debe 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 el VC2 directamente pasando por alto el VC1 y debería volver al controlador principal cuando se hace clic en un botón en el VC2. No hay código de ejecución larga, bucles o temporizadores. Solo llamadas de huesos para ver los controladores.

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 lo descarte. Cuando presenta un CV, crea una relación entre el CV que presenta y el presentado. Por lo tanto, no debe destruir el VC que lo presenta mientras está presentando (el VC presentado no puede enviar ese mensaje de rechazo …). Como realmente no lo estás tomando en cuenta, estás abandonando la aplicación en un estado confuso. Ver mi respuesta Descartar un controlador de vista presentado en el que recomiendo 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 mainVC . Debería usar un delegado para devolver el mensaje correcto a MainViewController desde ViewController1, para que mainVC pueda descartar VC1 y presentar VC2.

En VC2 VC1 agregue un protocolo en su archivo .h encima de la interfaz @:

 @protocol ViewController1Protocol  - (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  delegate; 

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

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

Ahora en mainVC, configúralo 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 de delegado:

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

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

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

actualizar

En sus comentarios, expresa sorpresa por la complejidad requerida para lograr algo aparentemente simple. Te aseguro que este patrón de delegación es tan central en gran parte de Objective-C y Cocoa, y este ejemplo es lo más simple que puedes obtener, que realmente debes hacer un esfuerzo para sentirte cómodo con él.

En la Guía de progtwigció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 presente lo descarte. En otras palabras, siempre que sea posible, el mismo controlador de vista que presentó el controlador de vista también debería asumir la responsabilidad de descartarlo. Aunque hay varias técnicas para notificar al controlador de vista que presenta que su controlador de vista presentado debe descartarse, la técnica preferida es la delegación. Para obtener más información, consulte “Usar 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 hacer todo el trabajo es la única salida lógica dado que no desea usar un NavigationController. Si usa NavController, en efecto está ‘delegando’, incluso si no explícitamente, en el navegador NavController para hacer todo el trabajo. Es necesario que haya algún objeto que mantenga una pista central de lo que está sucediendo con su navegación de VC, y necesita algún método de comunicación con él, haga lo que haga.

En la práctica, el consejo de Apple es un poco extremo … en casos normales, no necesita hacer un delegado y método dedicado, puede confiar en que [self presentingViewController] dismissViewControllerAnimated: – es cuando en casos como el suyo quiere que lo descarten tener otros efectos en objetos remotos que debe cuidar.

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

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

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

  • en VC1 usted no sabe que mainVC implementa el método present2 – puede terminar con errores o fallas difíciles de depurar. Los delegates te ayudan a evitar esto.
  • una vez que se descarta VC1, realmente no está a punto de ejecutar el bloque de finalización … ¿o no? Does self.presentingViewController significa algo más? Usted no sabe (tampoco yo) … con un delegado, usted no tiene esta incertidumbre.
  • Cuando bash ejecutar este método, simplemente cuelga sin advertencia o errores.

Entonces, por favor … tómense el tiempo para aprender delegación!

actualización2

En su comentario, ha logrado que funcione al usar esto en el controlador de botón de descartar de VC2:

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

Esto es ciertamente mucho más simple, pero te deja con una serie de problemas.

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

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

Memoria
VC1 no se ha ido, todavía tiene dos punteros a él:

  • la propiedad ViewController presentedViewController mainVC
  • La propiedad presentingViewController de VC2

Puede probar esto al iniciar sesión, y también solo haciendo esto desde VC2

 [self dismissViewControllerAnimated:YES completion:nil]; 

Todavía funciona, todavía te devuelve a VC1.

Eso me parece una fuga de memoria.

La clave para 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  // while a presentation or dismiss is in progress! 

La lógica se rompe, ya que está intentando descartar el VC de presentación del cual VC2 es el VC presentado. El segundo mensaje realmente no se ejecuta, bueno, quizás sucedan algunas cosas, pero aún le quedan dos punteros a un objeto del que pensó que se había deshecho. ( edit – He comprobado esto y no está tan mal, ambos objetos desaparecen cuando regresas a mainVC )

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

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

En VC1:

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

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

En VC2:

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

Como sabemos que no hemos descartado VC1, podemos regresar a través de VC1 a MainVC. MainVC descarta VC1. Debido a que VC1 se ha ido, se presenta que VC2 va con él, por lo que está de vuelta en MainVC en un estado limpio.

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

Creo que malinterpretaste algunos conceptos básicos sobre los controladores de vista modal iOS. Cuando descarta VC1, cualquier controlador de vista presentado por VC1 también se descarta. Apple pretendía que los controladores de visualización modal fluyeran de forma astackda; en su caso, VC1 presenta el VC1. Está descartando VC1 tan pronto como presente VC2 desde 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 el VC1 se descarte. Y creo que esto se puede lograr sin delegates. Algo en la línea:

 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 descarte, no debe hacer ninguna referencia a él.

Ejemplo en Swift , imaginando la explicación de la fundición arriba y la documentación de Apple:

  1. Basándose en la documentación de Apple y en la explicación de la fundición anterior (corrigiendo algunos errores), presente la versión de ViewController utilizando un 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! @IBAction func goToViewController2(sender: UIButton) { self.delegate.dismissViewController1AndPresentViewController2() } } 

ViewController2.swift

 import UIKit class ViewController2: UIViewController { } 
  1. Basándonos en la explicación de la fundición anterior (corrigiendo algunos errores), presione la versión de ViewViewController usando un 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! @IBAction func goToViewController2(sender: UIButton) { self.delegate.popViewController1AndPushViewController2() } } 

ViewController2.swift

 import UIKit class ViewController2: UIViewController { } 

Radu Simionescu: ¡trabajo increíble! y debajo de 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) }) } 

Yo quería esto:

MapVC es un mapa en pantalla completa.

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

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

Hice esto:

MapVC.m: en la acción del botón, un segue programmatically y set delegate

 - (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, agrega el protocolo

 @protocol PopupVCProtocol  - (void)dismissAndPresentMap; @end 

después de @interface, una nueva propiedad

 @property (nonatomic,weak) id  delegate; 

PopupVC.m:

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

He resuelto 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 hacer una animación de inserción por

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

Y en VC2, cuando cierre el controlador de vista, como de costumbre, podemos usar:

 self.dismiss(animated: true, completion: nil)