Modal View Controller - Anzeigen und Schließen


81

Ich habe mir in der letzten Woche den Kopf gebrochen, wie ich das Problem lösen kann, indem ich mehrere View Controller anzeige und entlasse. Ich habe ein Beispielprojekt erstellt und den Code direkt aus dem Projekt eingefügt. Ich habe 3 View Controller mit den entsprechenden .xib-Dateien. MainViewController, VC1 und VC2. Ich habe zwei Tasten auf dem Hauptansichts-Controller.

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

Dies öffnet VC1 ohne Probleme. In VC1 habe ich eine weitere Schaltfläche, die VC2 öffnen und gleichzeitig VC1 schließen soll.

- (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. 

Ich möchte, dass es zurück zum Hauptansichts-Controller geht, während gleichzeitig VC1 endgültig aus dem Speicher entfernt werden sollte. VC1 sollte nur angezeigt werden, wenn ich auf der Hauptsteuerung auf die Schaltfläche VC1 klicke.

Die andere Schaltfläche auf dem Hauptansichts-Controller sollte auch in der Lage sein, VC2 direkt unter Umgehung von VC1 anzuzeigen, und sollte zum Hauptcontroller zurückkehren, wenn auf VC2 auf eine Schaltfläche geklickt wird. Es gibt keinen lang laufenden Code, keine Schleifen oder Timer. Nur Bare-Bone-Aufrufe zum Anzeigen von Controllern.

Antworten:


189

Diese Linie:

[self dismissViewControllerAnimated:YES completion:nil];

sendet keine Nachricht an sich selbst, sondern sendet tatsächlich eine Nachricht an die präsentierende VC und fordert sie auf, die Entlassung vorzunehmen. Wenn Sie eine VC präsentieren, erstellen Sie eine Beziehung zwischen der präsentierenden und der präsentierten VC. Sie sollten die präsentierende VC also nicht zerstören, während sie präsentiert wird (die präsentierte VC kann diese Entlassungsnachricht nicht zurücksenden…). Da Sie dies nicht wirklich berücksichtigen, verlassen Sie die App in einem verwirrten Zustand. Siehe meine Antwort Das Ablehnen eines Presented View Controllers, in dem ich diese Methode empfehle, ist klarer geschrieben:

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

In Ihrem Fall müssen Sie sicherstellen, dass die gesamte Steuerung in erfolgt mainVC . Sie sollten einen Delegaten verwenden, um die richtige Nachricht von ViewController1 an MainViewController zurückzusenden, damit mainVC VC1 schließen und dann VC2 präsentieren kann.

Fügen Sie in VC2 VC1 ein Protokoll in Ihre .h-Datei über dem @interface ein:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

und unten in derselben Datei im Abschnitt @interface deklarieren Sie eine Eigenschaft, die den Delegatenzeiger enthält:

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

In der VC1 .m-Datei sollte die Entlassungsschaltflächenmethode die Delegatenmethode aufrufen

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

Legen Sie es jetzt in mainVC als VC1-Delegat fest, wenn Sie VC1 erstellen:

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

und implementieren Sie die Delegate-Methode:

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

present2: kann die gleiche Methode sein wie Ihre VC2Pressed: Button-IBAction-Methode. Beachten Sie, dass es vom Abschlussblock aufgerufen wird, um sicherzustellen, dass VC2 erst angezeigt wird, wenn VC1 vollständig geschlossen wurde.

Sie wechseln jetzt von VC1-> VCMain-> VC2, sodass wahrscheinlich nur einer der Übergänge animiert werden soll.

aktualisieren

In Ihren Kommentaren drücken Sie Ihre Überraschung über die Komplexität aus, die erforderlich ist, um eine scheinbar einfache Sache zu erreichen. Ich versichere Ihnen, dieses Delegationsmuster ist für einen Großteil von Objective-C und Cocoa so zentral, und dieses Beispiel ist so einfach wie möglich, dass Sie sich wirklich die Mühe machen sollten, sich damit vertraut zu machen.

In Apples Programming - View - Controller - Führer sie haben , dies zu sagen :

Verwerfen eines Presented View Controllers

Wenn es an der Zeit ist, einen Controller für präsentierte Ansichten zu schließen, besteht der bevorzugte Ansatz darin, den Controller für präsentierte Ansichten entlassen zu lassen. Mit anderen Worten, wann immer möglich, sollte derselbe Ansichtscontroller, der den Ansichtscontroller vorgestellt hat, auch die Verantwortung für die Entlassung übernehmen. Obwohl es verschiedene Techniken gibt, um den Controller für die präsentierende Ansicht zu benachrichtigen, dass sein Controller für die präsentierte Ansicht entlassen werden sollte, ist die Delegierung die bevorzugte Technik. Weitere Informationen finden Sie unter „Verwenden der Delegierung zur Kommunikation mit anderen Controllern“.

Wenn Sie wirklich überlegen, was Sie erreichen möchten und wie Sie vorgehen, werden Sie feststellen, dass das Versenden von Nachrichten an Ihren MainViewController, um die gesamte Arbeit zu erledigen, der einzig logische Ausweg ist, da Sie keinen NavigationController verwenden möchten. Wenn Sie tun eine NavController verwenden, sind in der Tat Sie ‚Delegieren‘, auch wenn nicht explizit auf die NavController die ganze Arbeit zu tun. Es muss ein Objekt geben, das zentral verfolgt, was mit Ihrer VC-Navigation vor sich geht, und Sie benötigen eine Kommunikationsmethode, unabhängig davon, was Sie tun.

In der Praxis ist der Rat von Apple ein wenig extrem. In normalen Fällen müssen Sie keinen dedizierten Delegaten und keine dedizierte Methode festlegen, auf die Sie sich verlassen können. [self presentingViewController] dismissViewControllerAnimated:In Fällen wie Ihrem möchten Sie, dass Ihre Entlassung andere Auswirkungen auf die Fernbedienung hat Objekte, auf die Sie achten müssen.

Hier ist etwas, das Sie sich vorstellen können , ohne den ganzen Aufwand der Delegierten zu arbeiten ...

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

}

Nachdem wir den präsentierenden Controller gebeten haben, uns zu entlassen, haben wir einen Abschlussblock, der eine Methode im PresentingViewController aufruft, um VC2 aufzurufen. Kein Delegierter erforderlich. (Ein großes Verkaufsargument von Blöcken ist, dass sie unter diesen Umständen den Bedarf an Delegierten verringern). In diesem Fall stehen jedoch einige Dinge im Weg ...

  • In VC1 wissen Sie nicht , dass mainVC die Methode implementiert. Dies present2kann zu schwer zu debuggenden Fehlern oder Abstürzen führen. Delegierte helfen Ihnen, dies zu vermeiden.
  • Sobald VC1 entlassen wurde, ist es nicht wirklich möglich, den Abschlussblock auszuführen ... oder? Bedeutet self.presentingViewController noch etwas? Sie wissen es nicht (ich auch nicht) ... mit einem Delegierten haben Sie diese Unsicherheit nicht.
  • Wenn ich versuche, diese Methode auszuführen, hängt sie nur ohne Warnung oder Fehler.

Also bitte ... nehmen Sie sich Zeit, um Delegation zu lernen!

update2

In Ihrem Kommentar haben Sie es geschafft, dass dies funktioniert, indem Sie dies im VC2-Handler für die Schaltfläche "Entlassen" verwenden:

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

Dies ist sicherlich viel einfacher, führt jedoch zu einer Reihe von Problemen.

Enge Kopplung
Sie verdrahten Ihre viewController-Struktur fest miteinander. Wenn Sie beispielsweise vor mainVC einen neuen viewController einfügen, wird Ihr erforderliches Verhalten unterbrochen (Sie würden zum vorherigen navigieren). In VC1 mussten Sie auch VC2 # importieren. Daher haben Sie ziemlich viele Abhängigkeiten, was die OOP / MVC-Ziele verletzt.

Bei Verwendung von Delegaten müssen weder VC1 noch VC2 etwas über mainVC oder seine Vorgeschichte wissen, sodass wir alles lose gekoppelt und modular halten.

Speicher
VC1 ist nicht verschwunden, Sie halten noch zwei Zeiger darauf:

  • presentedViewControllerEigentum von mainVC
  • presentingViewControllerEigentum von VC2

Sie können dies testen, indem Sie sich anmelden und dies auch einfach über VC2 tun

[self dismissViewControllerAnimated:YES completion:nil]; 

Es funktioniert immer noch und bringt Sie zurück zu VC1.

Das scheint mir ein Speicherverlust zu sein.

Der Hinweis darauf liegt in der Warnung, die Sie hier erhalten:

[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!

Die Logik bricht, wie Sie versuchen , die Präsentation VC zu entlassen , von denen VC2 ist die präsentiert VC. Die zweite Nachricht wird nicht wirklich ausgeführt - vielleicht passiert etwas, aber Sie haben immer noch zwei Zeiger auf ein Objekt, von dem Sie dachten, Sie hätten es losgeworden. ( Bearbeiten - Ich habe dies überprüft und es ist nicht so schlimm, beide Objekte verschwinden, wenn Sie zu mainVC zurückkehren. )

Das ist eine ziemlich langatmige Art zu sagen - bitte verwenden Sie Delegierte. Wenn es hilft, habe ich das Muster hier noch einmal kurz beschrieben:
Ist es immer eine schlechte Praxis, einen Controller in einem Construtor zu übergeben?

Update 3
Wenn Sie Delegierte wirklich vermeiden möchten, ist dies der beste Ausweg:

In VC1:

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

Aber nicht alles abtun ... wie wir festgestellt, dass es nicht sowieso passieren.

In VC2:

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

Da wir (wissen), dass wir VC1 nicht entlassen haben, können wir über VC1 zu MainVC zurückkehren. MainVC entlässt VC1. Da VC1 weg ist, wird es mit VC2 präsentiert, sodass Sie wieder in einem sauberen Zustand bei MainVC sind.

Es ist immer noch stark gekoppelt, da VC1 über VC2 Bescheid wissen muss und VC2 wissen muss, dass es über MainVC-> VC1 erreicht wurde, aber es ist das Beste, was Sie ohne ein wenig explizite Delegierung erhalten werden.


1
scheint kompliziert zu sein. Ich habe versucht zu folgen und auf den Punkt zu kopieren, bin aber in der Mitte verloren gegangen. Gibt es eine andere Möglichkeit, dies zu erreichen? Ich wollte auch hinzufügen, dass im App-Delegaten der Hauptcontroller als Root-View-Controller festgelegt ist. Ich möchte keine Navigationssteuerungen verwenden, frage mich aber, warum dies so kompliziert zu erreichen sein sollte. Zusammenfassend zeige ich beim Start der App einen Hauptansichts-Controller mit 2 Schaltflächen. Durch Klicken auf die erste Schaltfläche wird VC1 geladen. Auf VC1 befindet sich eine Schaltfläche, und beim Klicken sollte VC2 ohne Fehler oder Warnungen geladen werden, während VC1 gleichzeitig aus dem Speicher entfernt wird.
Hema

Auf VC2 habe ich eine Schaltfläche, und durch Klicken darauf sollte VC2 aus dem Speicher entfernt werden, und die Steuerung sollte zum Hauptcontroller zurückkehren und nicht zu VC1.
Hema

@Hema, ich habe Ihre Anforderungen perfekt verstanden, und versichern Sie dies ist der richtige Weg , es zu tun. Ich habe meine Antwort mit ein bisschen mehr Infos aktualisiert, hoffe das hilft. Wenn Sie meinen Ansatz ausprobiert haben und nicht weiterkommen, stellen Sie bitte eine neue Frage, die genau zeigt, was nicht funktioniert, damit wir Ihnen helfen können. Sie können aus Gründen der Übersichtlichkeit auch auf diese Frage zurückgreifen.
Gießerei

Hi He Was: Danke für deinen Einblick. Ich spreche auch über einen anderen Thread (Original-Thread) und habe gerade einen Ausschnitt aus den dort genannten Vorschlägen gepostet. Ich versuche alle Expertenantworten, um dieses Problem zu lösen. Die URL ist hier: stackoverflow.com/questions/14840318/…
Hema

1
@Honey - Vielleicht, aber die Aussage war eine rhetorische Antwort auf ein Stück 'imaginierten' Pseudocodes. Der Punkt, den ich ansprechen wollte, ist nicht, Zyklusfallen beizubehalten, sondern den Fragesteller darüber aufzuklären, warum Delegation ein wertvolles Entwurfsmuster ist (wodurch dieses Problem im Übrigen vermieden wird). Ich denke, das ist die irreführende Behauptung hier - die Frage betrifft modale VCs, aber der Wert der Antwort liegt hauptsächlich in der Erklärung des Delegiertenmusters unter Verwendung der Frage und der offensichtlichen Frustrationen des OP als Katalysator. Vielen Dank für Ihr Interesse (und Ihre Änderungen)!
Gießerei

12

Beispiel in Swift mit der obigen Erklärung der Gießerei und der Dokumentation von Apple:

  1. Basierend auf der Dokumentation von Apple und der obigen Erklärung der Gießerei (Korrektur einiger Fehler) präsentiert die PresentViewController-Version das Delegate-Entwurfsmuster:

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. Basierend auf der obigen Erklärung der Gießerei (Korrektur einiger Fehler), PushViewController-Version unter Verwendung des Delegate-Entwurfsmusters:

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 {

}

in deiner Beispielklasse ViewControllerist mainVC richtig?
Honig

10

Ich denke, Sie haben einige Kernkonzepte über iOS Modal View Controller falsch verstanden. Wenn Sie VC1 schließen, werden auch alle von VC1 präsentierten Ansichtscontroller entlassen. Apple wollte, dass Controller mit modaler Ansicht gestapelt fließen - in Ihrem Fall wird VC2 von VC1 präsentiert. Sie schließen VC1, sobald Sie VC2 von VC1 präsentieren, so dass es ein totales Chaos ist. Um das zu erreichen, was Sie wollen, sollte bei buttonPressedFromVC1 der mainVC VC2 unmittelbar nach dem Entladen von VC1 vorhanden sein. Und ich denke, dies kann ohne Delegierte erreicht werden. Etwas in der Richtung:

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

Beachten Sie, dass self.presentingViewController in einer anderen Variablen gespeichert ist, da Sie nach dem Schließen von vc1 keine Verweise darauf machen sollten.


1
so einfach! Ich wünschte, andere würden nach unten zu Ihrer Antwort scrollen, anstatt am obersten Beitrag anzuhalten.
Ryan Loggerythm

in dem Code des OP, warum nicht [self dismiss...]passieren , nachdem [self present...] fertig ist? Es ist nicht so, dass etwas Asynchrones passiert
Honey

1
@Honey tatsächlich passiert beim Aufrufen von presentViewController etwas Asynchrones - deshalb gibt es einen Completion-Handler. Aber selbst wenn Sie den Controller für die präsentierende Ansicht schließen, nachdem er etwas präsentiert hat, wird auch alles, was er präsentiert, entlassen. Also möchte OP den Viewcontroller tatsächlich von einem anderen Moderator präsentieren, damit er den aktuellen entlassen kann
Radu Simionescu

Aber selbst wenn Sie das verwenden, wenn Sie den Controller für die präsentierende Ansicht schließen, nachdem er etwas präsentiert hat, wird auch alles, was er präsentiert, entlassen ... Aha, also sagt der Compiler im Grunde: "Was Sie tun, ist dumm. Sie haben gerade Ihre vorherige rückgängig gemacht." Codezeile (als VC1 entlasse ich mich und was auch immer ich präsentiere). Tu es nicht "richtig?
Honig

Der Compiler "sagt" nichts darüber, und es kann auch vorkommen, dass er beim Ausführen nicht abstürzt, nur dass er sich so verhält, wie es der Programmierer nicht erwartet
Radu Simionescu

5

Radu Simionescu - tolle Arbeit! und darunter Ihre Lösung für Swift-Liebhaber:

@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)
    })
}

Dies macht mich in gewisser Weise frustriert, dass es tatsächlich funktioniert. Ich verstehe nicht, warum der Block nicht "self.presentingViewController" erfasst und eine starke Referenz benötigt wird, dh "var PresentingVC". Wie auch immer, das funktioniert. thx
emdog4

1

Ich wollte das:

MapVC ist eine Karte im Vollbildmodus.

Wenn ich eine Taste drücke, wird PopupVC (nicht im Vollbildmodus) über der Karte geöffnet.

Wenn ich in PopupVC eine Schaltfläche drücke, kehrt sie zu MapVC zurück, und dann möchte ich viewDidAppear ausführen.

Ich war das:

MapVC.m: In der Schaltflächenaktion wird ein Übergang programmgesteuert und ein Delegat festgelegt

- (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: Fügen Sie vor @interface das Protokoll hinzu

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

nach @interface eine neue Eigenschaft

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

PopupVC.m:

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

1

Ich habe das Problem mithilfe von UINavigationController bei der Präsentation gelöst. In MainVC bei der Präsentation von VC1

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

Wenn ich in VC1 VC2 anzeigen und VC1 gleichzeitig schließen möchte (nur eine Animation), kann ich eine Push-Animation von haben

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

Und in VC2 können wir beim Schließen des View Controllers wie gewohnt Folgendes verwenden:

self.dismiss(animated: true, completion: nil)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.