web-dev-qa-db-fra.com

contrôleurs de vues modales - Comment afficher et ignorer

La semaine dernière, je me suis cassé la tête pour résoudre le problème de l'affichage et de la suppression de plusieurs contrôleurs de vue. J'ai créé un exemple de projet et collé le code directement à partir du projet. J'ai 3 contrôleurs de vue avec leurs fichiers .xib correspondants. MainViewController, VC1 et VC2. J'ai deux boutons sur le contrôleur de vue principale.

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

Cela ouvre VC1 sans aucun problème. Dans VC1, j'ai un autre bouton qui devrait ouvrir VC2 tout en renvoyant 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. 

Je veux qu'il revienne au contrôleur de la vue principale tout en même temps que VC1 aurait dû être supprimé de la mémoire pour de bon. VC1 ne devrait apparaître que lorsque je clique sur le bouton VC1 du contrôleur principal.

L'autre bouton du contrôleur de vue principale doit également pouvoir afficher VC2 en contournant directement VC1 et doit revenir au contrôleur principal lorsqu'un clic est effectué sur VC2. Il n'y a pas de code long en cours d'exécution, des boucles ou des minuteries. Il suffit d'appeler les os pour voir les contrôleurs.

79
Hema

Cette ligne:

[self dismissViewControllerAnimated:YES completion:nil];

n'envoie pas de message à lui-même, il envoie en fait un message à son VC qui le présente, lui demandant de faire le renvoi. Lorsque vous présentez un VC, vous créez une relation entre le VC qui présente et celui présenté. Donc, vous ne devriez pas détruire le VC présentateur pendant la présentation (le VC présenté _ ne peut pas renvoyer ce message de renvoi…). Comme vous n'en tenez pas vraiment compte, vous laissez l'application dans un état confus. Voir ma réponse Rejeter un contrôleur de vue présentée dans laquelle je recommande cette méthode est plus clairement écrite:

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

Dans votre cas, vous devez vous assurer que tout le contrôle est effectué dans mainVC. Vous devez utiliser un délégué pour renvoyer le message correct à MainViewController à partir de ViewController1, afin que mainVC puisse ignorer VC1 puis présenter VC2.

Dans VC2 VC1 ajoute un protocole dans votre fichier .h au-dessus de @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

et plus bas dans le même fichier de la section @interface, déclarez une propriété pour contenir le pointeur de délégué:

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

Dans le fichier VC1 .m, la méthode du bouton de rejet doit appeler la méthode du délégué.

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

Maintenant dans mainVC, définissez-le comme délégué de VC1 lors de la création de VC1:

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

et implémenter la méthode delegate:

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

present2: peut être la même méthode que votre méthode VC2Pressed: avec le bouton IBAction. Notez qu'il est appelé à partir du bloc d'achèvement pour s'assurer que VC2 n'est pas présenté jusqu'à ce que VC1 soit complètement rejeté.

Vous vous déplacez maintenant de VC1-> VCMain-> VC2, de sorte que vous souhaiterez probablement qu'une seule des transitions soit animée.

mettre à jour

Dans vos commentaires, vous êtes surpris de la complexité nécessaire pour réaliser une chose apparemment simple. Je vous assure que ce modèle de délégation est si essentiel à une grande partie d'Objective-C et de Cocoa, et cet exemple est le plus simple qui soit, vous devez vraiment faire l'effort de vous mettre à l'aise avec cela.

Dans le Guide de programmation du contrôleur View View , ils ont ceci à dire :

Rejeter un contrôleur de vue présentée

Lorsque vient le temps de rejeter un contrôleur de vue présenté, l’approche privilégiée consiste à laisser le contrôleur de vue présent le rejeter. En d'autres termes, chaque fois que cela est possible, le même contrôleur de vue que celui qui l'a présenté doit également assumer la responsabilité de le rejeter. Bien qu'il existe plusieurs techniques pour notifier au contrôleur de vue présentation que son contrôleur de vue présenté doit être congédié, la technique recommandée est la délégation. Pour plus d'informations, voir "Utilisation de la délégation pour communiquer avec d'autres contrôleurs".

Si vous réfléchissez vraiment à ce que vous voulez réaliser et à la façon dont vous vous y prenez, vous vous rendrez compte que l'envoi par messagerie de MainViewController pour effectuer tout le travail est la seule solution logique étant donné que vous ne souhaitez pas utiliser un NavigationController. Si vous utilisez un NavController, vous "déléguez" en fait, même si ce n’est pas explicitement, à navController pour effectuer tout le travail. Il doit y avoir un objet qui garde une trace centrale de ce qui se passe avec votre navigation VC, et vous avez besoin de méthode de communication avec elle, quoi que vous fassiez.

Dans la pratique, les conseils d'Apple sont un peu extrêmes. Normalement, vous n'avez pas besoin de désigner un délégué et une méthode dédiés, vous pouvez vous fier à [self presentingViewController] dismissViewControllerAnimated: - c'est dans des cas comme le vôtre que vous souhaitez que votre congédiement avoir d'autres effets sur les objets distants dont vous devez prendre soin.

Voici quelque chose que vous pourriez imaginer pour fonctionner sans tous les tracas du délégué ...

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

}

Après avoir demandé au contrôleur de présentation de nous renvoyer, nous avons un bloc d'achèvement qui appelle une méthode dans PresentationViewController pour appeler VC2. Aucun délégué nécessaire. (Un gros argument de vente des blocs est qu'ils réduisent le besoin de délégués dans ces circonstances). Cependant, dans ce cas, il y a quelques choses qui gênent ...

  • dans VC1, vous ne savez pas que mainVC implémente la méthode present2 - vous pouvez vous retrouver avec des erreurs ou des plantages difficiles à déboguer. Les délégués vous aident à éviter cela.
  • une fois que VC1 est licencié, l'exécution du bloc d'achèvement n'est plus vraiment utile ... ou s'agit-il? Est-ce que self.presentingViewController signifie plus? Vous ne savez pas (moi non plus) ... avec un délégué, vous n'avez pas cette incertitude.
  • Lorsque j'essaie d'exécuter cette méthode, elle se bloque sans avertissement ni erreur.

Alors s'il vous plaît ... prenez le temps d'apprendre la délégation!

update2

Dans votre commentaire, vous avez réussi à le faire fonctionner en l'utilisant dans le gestionnaire de boutons de fermeture de VC2:

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

C’est certes beaucoup plus simple, mais cela vous laisse un certain nombre de problèmes.

Couplage étroit
Vous êtes en train de câbler ensemble la structure de votre viewController. Par exemple, si vous deviez insérer un nouveau viewController avant mainVC, votre comportement requis serait rompu (vous iriez au précédent). Dans VC1, vous avez également dû importer # VC2. Par conséquent, vous avez beaucoup d'interdépendances, ce qui casse les objectifs de POO/MVC.

À l’aide de délégués, ni VC1 ni VC2 n’ont besoin de savoir quoi que ce soit sur mainVC ou ses antécédents, nous conservons donc tout ce qui est couplé et modulaire.

mémoire
VC1 n’est pas parti, vous avez toujours deux indicateurs:

  • la propriété mainVC presentedViewController
  • Propriété presentingViewController de VC2

Vous pouvez tester cela en vous connectant, et aussi simplement en faisant cela depuis VC2

[self dismissViewControllerAnimated:YES completion:nil]; 

Cela fonctionne toujours, vous renvoie toujours à VC1.

Cela me semble être une fuite de mémoire.

L'indice à ce sujet est dans l'avertissement que vous obtenez ici:

[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 logique échoue, car vous essayez de rejeter le VC présentateur, dont VC2 est le VC présenté. Le second message n'est pas vraiment exécuté - il se peut que quelque chose se passe, mais il vous reste deux pointeurs sur un objet dont vous pensiez vous être débarrassé. ( edit - J'ai vérifié ceci et ce n'est pas si grave, les deux objets disparaissent quand vous revenez à mainVC )

C’est une façon assez longue de dire - veuillez utiliser les délégués. Si cela peut aider, j'ai fait une autre brève description du motif ici:
Est-ce que passer un contrôleur dans un constructeur est toujours une mauvaise pratique?

mise à jour 3
Si vous voulez vraiment éviter les délégués, voici le meilleur moyen de sortir:

Dans VC1:

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

Mais ne pas rejeter quoi que ce soit ... comme nous l'avons constaté, cela ne se produit pas vraiment de toute façon.

Dans VC2:

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

Comme nous le savons, nous n’avons pas rejeté VC1, nous pouvons revenir à travers VC1 à MainVC. MainVC rejette VC1. Parce que VC1 a disparu, il est présenté que VC2 va avec, vous êtes donc de retour à MainVC dans un état propre.

C'est toujours très couplé, car VC1 a besoin de connaître VC2 et VC2 a besoin de savoir qu'il a été obtenu via MainVC-> VC1, mais c'est le meilleur avantage que vous obtiendrez sans délégation un peu explicite.

188
foundry

Exemple dans Swift, illustrant l'explication de la fonderie ci-dessus et la documentation d'Apple:

  1. En vous basant sur la documentation Apple et l'explication de la fonderie .__ ci-dessus (en corrigeant quelques erreurs), presentViewController Version à l'aide d'un modèle de conception délégué:

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. En se basant sur l'explication de la fonderie ci-dessus (corrigeant quelques erreurs), la version PushViewController utilisant un modèle de conception de délégué:

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 {

}
9
King-Wizard

Je pense que vous avez mal compris certains concepts de base concernant les contrôleurs de vue modale iOS. Lorsque vous quittez VC1, tous les contrôleurs de vue présentés par VC1 sont également rejetés. Apple a prévu que les contrôleurs de vue modale fonctionnent de manière empilée - dans votre cas, VC2 est présenté par VC1. Vous quittez VC1 dès que vous présentez VC2 à partir de VC1, c’est donc un désordre total… .. Pour obtenir ce que vous voulez, buttonPressedFromVC1 doit avoir VC présenté par VCV immédiatement après que VC1 se soit révoqué. Et je pense que cela peut être réalisé sans délégués. Quelque chose le long des lignes:

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

Notez que self.presentingViewController est stocké dans une autre variable, car une fois que vc1 se sera rejeté, vous ne devriez plus y faire référence.

9
Radu Simionescu

Radu Simionescu - super travail! et ci-dessous Votre solution pour les amoureux 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)
    })
}
4
chrisco

Je voulais ceci:

MapVC est une carte en plein écran. 

Lorsque j'appuie sur un bouton, PopupVC s'ouvre (pas en plein écran) au-dessus de la carte.

Lorsque j'appuie sur un bouton dans PopupVC, il retourne à MapVC, puis je souhaite exécuter viewDidAppear.

J'ai fait ça:

MapVC.m: dans l'action du bouton, passer d'un programme à un autre et définir un délégué

- (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: avant @interface, ajoutez le protocole

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

après @interface, une nouvelle propriété

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

PopupVC.m: 

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

J'ai résolu le problème en utilisant UINavigationController lors de la présentation de . Dans MainVC, lors de la présentation de VC1

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

Dans VC1, lorsque je souhaite montrer VC2 et supprimer VC1 en même temps (une seule animation), je peux avoir une animation Push de 

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

Et dans VC2, lorsque vous fermez le contrôleur de vue, vous pouvez utiliser comme d'habitude:

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