Kontrolery widoku modalnego - jak wyświetlać i odrzucać

82

Przez ostatni tydzień łamałem sobie głowę, jak rozwiązać problem z wyświetlaniem i odrzucaniem wielu kontrolerów widoku. Stworzyłem przykładowy projekt i wkleiłem kod bezpośrednio z projektu. Mam 3 kontrolery widoku z odpowiadającymi im plikami .xib. MainViewController, VC1 i VC2. Mam dwa przyciski na głównym kontrolerze widoku.

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

To otwiera VC1 bez żadnych problemów. W VC1 mam inny przycisk, który powinien otworzyć VC2, a jednocześnie odrzucić 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. 

Chcę, żeby wrócił do głównego kontrolera widoku, podczas gdy w tym samym czasie VC1 powinien zostać usunięty z pamięci na dobre. VC1 powinien pojawić się tylko wtedy, gdy kliknę przycisk VC1 na głównym kontrolerze.

Drugi przycisk na kontrolerze widoku głównego powinien również być w stanie wyświetlić VC2 bezpośrednio omijając VC1 i powinien powrócić do głównego kontrolera po kliknięciu przycisku na VC2. Nie ma długo działającego kodu, pętli ani żadnych timerów. Tylko nagie wezwania do obejrzenia kontrolerów.

Hema
źródło

Odpowiedzi:

189

Ta linia:

[self dismissViewControllerAnimated:YES completion:nil];

nie wysyła wiadomości do siebie, w rzeczywistości wysyła wiadomość do prezentującego VC, prosząc go o odrzucenie. Przedstawiając VC, tworzysz relację między prezentującym VC a prezentowanym. Dlatego nie powinieneś niszczyć prezentującego VC podczas prezentacji (przedstawiony VC nie może odesłać tej wiadomości odrzucającej…). Ponieważ tak naprawdę nie bierzesz tego pod uwagę, pozostawiasz aplikację w stanie zdezorientowania. Zobacz moją odpowiedź Dismissing a Presented View Controller, w którym polecam tę metodę, jest jaśniej napisana:

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

W Twoim przypadku musisz upewnić się, że cała kontrola została wykonana mainVC . Należy użyć delegata, aby wysłać poprawny komunikat z powrotem do MainViewController z ViewController1, aby mainVC mógł odrzucić VC1, a następnie przedstawić VC2.

W VC2 VC1 dodaj protokół w swoim pliku .h powyżej @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

i niżej w tym samym pliku w sekcji @interface zadeklaruj właściwość do przechowywania wskaźnika delegata:

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

W pliku .m VC1 metoda przycisku odrzucenia powinna wywołać metodę delegata

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

Teraz w mainVC ustaw go jako delegata VC1 podczas tworzenia VC1:

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

i zaimplementuj metodę delegata:

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

present2:może być tą samą metodą, co metoda VC2Pressed:przycisku IBAction. Zauważ, że jest on wywoływany z bloku uzupełniania, aby zapewnić, że VC2 nie zostanie zaprezentowany, dopóki VC1 nie zostanie całkowicie odrzucony.

Przechodzisz teraz z VC1-> VCMain-> VC2, więc prawdopodobnie będziesz chciał animować tylko jedno przejście.

aktualizacja

W swoich komentarzach wyrażasz zdziwienie złożonością wymaganą do osiągnięcia pozornie prostej rzeczy. Zapewniam cię, że ten wzorzec delegowania jest tak centralny dla większości Objective-C i Cocoa, a ten przykład jest najprostszym, jaki możesz uzyskać, że naprawdę powinieneś dołożyć starań, aby przyzwyczaić się do niego.

W Apple View Controller Programming Guide mają do powiedzenia :

Odrzucanie kontrolera widoku prezentacji

Kiedy przychodzi czas na odrzucenie kontrolera widoku prezentowanego, preferowanym podejściem jest pozwolenie kontrolerowi widoku prezentacji na odrzucenie go. Innymi słowy, gdy tylko jest to możliwe, ten sam kontroler widoku, który przedstawił kontrolera widoku, powinien również wziąć odpowiedzialność za odwołanie go. Chociaż istnieje kilka technik powiadamiania kontrolera widoku prezentującego, że jego kontroler widoku prezentowanego powinien zostać odrzucony, preferowaną techniką jest delegowanie. Aby uzyskać więcej informacji, zobacz „Używanie delegowania do komunikacji z innymi kontrolerami”.

Jeśli naprawdę zastanowisz się, co chcesz osiągnąć i jak sobie z tym poradzisz, zdasz sobie sprawę, że wysyłanie wiadomości do MainViewController w celu wykonania całej pracy jest jedynym logicznym wyjściem, biorąc pod uwagę, że nie chcesz używać NavigationController. Jeśli zrobić użyć NavController w efekcie jesteś „delegowanie”, nawet jeśli nie wprost, do navController zrobić wszystkie prace. Musi istnieć jakiś obiekt, który centralnie śledzi to, co dzieje się z Twoją nawigacją VC, i potrzebujesz jakiejś metody komunikacji z nim, cokolwiek robisz.

W praktyce rada Apple jest trochę ekstremalna ... w normalnych przypadkach nie musisz wyznaczać dedykowanego delegata i metody, na której możesz polegać [self presentingViewController] dismissViewControllerAnimated:- to w przypadkach takich jak twój chcesz, aby twoje zwolnienie miało inny wpływ na pilota przedmioty, na które musisz uważać.

Oto coś, co możesz sobie wyobrazić, aby pracować bez kłopotów ze strony delegata ...

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

}

Po poproszeniu kontrolera Presentation o zwolnienie nas, mamy blok uzupełniania, który wywołuje metodę w PresentViewController w celu wywołania VC2. Nie potrzeba żadnego delegata. (Dużą zaletą bloków jest to, że zmniejszają potrzebę delegowania w takich okolicznościach). Jednak w tym przypadku kilka rzeczy staje na przeszkodzie ...

  • w VC1 nie wiesz, że mainVC implementuje metodę present2- możesz skończyć z trudnymi do debugowania błędami lub awariami. Delegaci pomogą ci tego uniknąć.
  • po odrzuceniu VC1 tak naprawdę nie ma go w pobliżu, aby wykonać blok uzupełniania ... czy tak jest? Czy self.presentingViewController już coś znaczy? Nie wiesz (ja też nie) ... z delegatem nie masz tej niepewności.
  • Kiedy próbuję uruchomić tę metodę, po prostu zawiesza się bez ostrzeżenia i błędów.

Więc proszę ... poświęć trochę czasu na naukę delegacji!

aktualizacja2

W swoim komentarzu udało Ci się to zadziałać, używając tego w programie obsługi przycisku VC2:

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

Jest to z pewnością znacznie prostsze, ale pozostawia wiele problemów.

Mocne sprzężenie
Twórz ze sobą sztywne okablowanie struktury viewController. Na przykład, jeśli chcesz wstawić nowy kontroler viewController przed mainVC, Twoje wymagane zachowanie ulegnie awarii (przejdziesz do poprzedniego). W VC1 musiałeś również #importować VC2. Dlatego masz sporo współzależności, co łamie cele OOP / MVC.

Używając delegatów, ani VC1, ani VC2 nie muszą nic wiedzieć o mainVC ani o jego poprzednikach, więc zachowujemy wszystko luźno powiązane i modułowe.

Pamięć
VC1 nie zniknęła, nadal masz do niej dwie wskazówki:

  • presentedViewControllerwłaściwość mainVC
  • presentingViewControllerWłasność VC2

Możesz to przetestować, logując się, a także robiąc to z VC2

[self dismissViewControllerAnimated:YES completion:nil]; 

Nadal działa, nadal przenosi Cię z powrotem do VC1.

Wydaje mi się, że to wyciek pamięci.

Wskazówka do tego znajduje się w ostrzeżeniu, które otrzymujesz:

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

Logika się psuje, ponieważ próbujesz odrzucić prezentujący VC, którego VC2 jest prezentowanym VC. Druga wiadomość tak naprawdę nie jest wykonywana - cóż, być może coś się dzieje, ale nadal pozostają dwa wskaźniki do obiektu, o którym myślałeś, że się pozbyłeś. ( edytuj - sprawdziłem to i nie jest tak źle, oba obiekty znikają, gdy wrócisz do mainVC )

To dość rozwlekły sposób powiedzenia - proszę, skorzystajcie z delegatów. Jeśli to pomaga, przedstawiłem tutaj kolejny krótki opis wzoru:
Czy przekazanie kontrolera u konstruktora jest zawsze złą praktyką?

aktualizacja 3
Jeśli naprawdę chcesz uniknąć delegatów, może to być najlepsze wyjście:

W VC1:

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

Ale nie odrzucaj niczego ... jak ustaliliśmy, tak naprawdę to się nie zdarza.

W VC2:

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

Ponieważ (wiemy) nie odrzuciliśmy VC1, możemy wrócić przez VC1 do MainVC. MainVC odrzuca VC1. Ponieważ VC1 zniknął, przedstawiono VC2 razem z nim, więc jesteś z powrotem w MainVC w stanie czystym.

Nadal jest silnie sprzężony, ponieważ VC1 musi wiedzieć o VC2, a VC2 musi wiedzieć, że został osiągnięty przez MainVC-> VC1, ale jest to najlepsze, co uzyskasz bez odrobiny wyraźnej delegacji.

odlewnia
źródło
1
wydaje się być skomplikowana. Próbowałem śledzić i kopiować do kropki, ale zgubiłem się w środku. Czy można to osiągnąć w inny sposób? Chciałem również dodać, że w delegacie aplikacji główny kontroler jest ustawiony jako główny kontroler widoku. Nie chcę używać kontrolerów nawigacji, ale zastanawiam się, dlaczego powinno to być tak skomplikowane. Podsumowując, po uruchomieniu aplikacji pokazuję główny kontroler widoku z 2 przyciskami. Kliknięcie pierwszego przycisku powoduje załadowanie VC1. Na VC1 znajduje się przycisk, który po kliknięciu powinien załadować VC2 bez błędów i ostrzeżeń, jednocześnie usuwając VC1 z pamięci.
Hema
Na VC2 mam przycisk i kliknięcie go powinno wyrzucić VC2 z pamięci i sterowanie powinno wrócić do kontrolera głównego a nie do VC1.
Hema
@Hema, doskonale zrozumiałem Twoje wymagania i zapewniam, że jest to właściwy sposób, aby to zrobić. Zaktualizowałem moją odpowiedź o trochę więcej informacji, mam nadzieję, że to pomoże. Jeśli wypróbowałeś moje podejście i utknąłeś, zadaj nowe pytanie, pokazujące dokładnie, co nie działa, abyśmy mogli pomóc. Możesz również podać link do tego pytania dla jasności.
odlewnia
Cześć, był: Dzięki za wgląd. Mówię też w innym wątku (wątek oryginalny) i właśnie opublikowałem fragment z sugestii tam wymienionych. Próbuję wszystkich odpowiedzi ekspertów, aby rozwiązać ten problem. Adres URL jest tutaj: stackoverflow.com/questions/14840318/…
Hema
1
@ Kochanie - Być może tak, ale stwierdzenie to było retoryczną odpowiedzią na fragment „wyimaginowanego” pseudokodu. Punkt, który chciałem poruszyć, nie dotyczy zachowania pułapek cykli, ale uświadomienia pytającemu, dlaczego delegowanie jest cennym wzorcem projektowym (który, nawiasem mówiąc, pozwala uniknąć tego problemu). Myślę, że jest to myląca teza - pytanie dotyczy modalnych VC, ale wartość odpowiedzi polega głównie na wyjaśnieniu wzorca delegatów, przy użyciu pytania i ewidentnych frustracji PO jako katalizatora. Dziękujemy za zainteresowanie (i zmiany) !!
odlewnia
12

Przykład w języku Swift , ilustrujący powyższe wyjaśnienie odlewni i dokumentację Apple:

  1. Bazując na dokumentacji firmy Apple i powyższym wyjaśnieniu odlewni (poprawienie niektórych błędów), wersja presentViewController wykorzystująca wzorzec projektowy delegata:

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. Bazując na powyższym wyjaśnieniu odlewni (poprawienie niektórych błędów), wersja pushViewController wykorzystująca wzorzec projektowy delegata:

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 {

}
King-Wizard
źródło
w twoim przykładzie ViewControllerklasa to mainVC, prawda?
Kochanie,
10

Myślę, że źle zrozumiałeś niektóre podstawowe koncepcje dotyczące kontrolerów widoku modalnego iOS. Kiedy odrzucasz VC1, wszystkie prezentowane kontrolery widoku przez VC1 są również odrzucane. Apple przeznaczone dla kontrolerów widoku modalnego do przepływu w sposób skumulowany - w twoim przypadku VC2 jest prezentowane przez VC1. Odrzucasz VC1, gdy tylko prezentujesz VC2 z VC1, więc jest to totalny bałagan. Aby osiągnąć to, co chcesz, buttonPressedFromVC1 powinien mieć mainVC obecny VC2 natychmiast po odrzuceniu VC1. Myślę, że można to osiągnąć bez delegatów. Coś w stylu:

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

Zauważ, że self.presentingViewController jest przechowywany w jakiejś innej zmiennej, ponieważ po odrzuceniu vc1 nie należy tworzyć żadnych odniesień do niego.

Radu Simionescu
źródło
1
tak prosty! Chciałbym, żeby inni przewijali w dół do Twojej odpowiedzi, zamiast zatrzymywać się na najwyższym poście.
Ryan Loggerythm
w kodzie OP, dlaczego nie [self dismiss...]dzieje się po [self present...] zakończeniu? To nie jest coś asynchronicznego
Honey
1
@ Kochanie właściwie, dzieje się coś asynchronicznego podczas wywoływania presentViewController - dlatego ma on moduł obsługi zakończenia. Ale nawet używając tego, jeśli odrzucisz kontroler widoku prezentacji po tym, jak coś przedstawia, wszystko, co przedstawia, również zostanie odrzucone. Więc OP właściwie chce zaprezentować kontroler widoku od innego prezentera, aby mógł odrzucić obecnego
Radu Simionescu
Ale nawet używając tego, jeśli odrzucisz kontroler widoku prezentacji po tym, jak coś przedstawia, wszystko, co przedstawia, również zostanie odrzucone ... Aha, więc kompilator w zasadzie mówi „to, co robisz, jest głupie. Po prostu cofnąłeś poprzednie wiersz kodu (jako VC1 odrzucę siebie i wszystko, co przedstawiam). Nie rób tego „prawda?
Miód
Kompilator nie "powie" nic na ten temat i może się zdarzyć, że nie wyłączy się podczas wykonywania tego, tylko że będzie się zachowywał w sposób, którego programista się nie spodziewa
Radu Simionescu
5

Radu Simionescu - świetna robota! a poniżej Twoje rozwiązanie dla miłośników Swifta:

@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
źródło
to w pewnym sensie denerwuje mnie, że to faktycznie działa .. Nie rozumiem, dlaczego blok nie przechwytuje „self.presentingViewController” i potrzebne jest silne odniesienie, np. „var presentVC”… w każdym razie to działa. thx
emdog4
1

Chciałem tego:

MapVC to mapa na pełnym ekranie.

Kiedy naciskam przycisk, otwiera się PopupVC (nie na pełnym ekranie) nad mapą.

Kiedy naciskam przycisk w PopupVC, wraca do MapVC, a następnie chcę wykonać viewDidAppear.

Ja to zrobiłem:

MapVC.m: w akcji przycisku, segue programowo i ustaw delegata

- (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: przed @interface dodaj protokół

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

po @interface: nowa właściwość

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

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}
Mer
źródło
1

Rozwiązałem ten problem za pomocą UINavigationController podczas prezentacji. W MainVC, prezentując VC1

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

W VC1, gdy chciałbym pokazać VC2 i odrzucić VC1 w tym samym czasie (tylko jedna animacja), mogę mieć animację wypychania przez

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

A w VC2 po zamknięciu kontrolera widoku, jak zwykle, możemy użyć:

self.dismiss(animated: true, completion: nil)
Duong Ngo
źródło