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

Łamię głowę przez ostatni tydzień, jak rozwiązać problem z wyświetlaniem i odrzucaniem wielu kontrolerów widoku. Stworzyłem przykładowy projekt i wklejam kod bezpośrednio z projektu. Mam 3 Kontrolery widoku z ich odpowiednikami .pliki 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 jeszcze jeden 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ę, aby wrócił do głównego kontrolera widoku, a jednocześnie VC1 powinien zostać usunięty z pamięci na dobre. VC1 powinien pojawić się dopiero po kliknięciu przycisku VC1 na głównym kontrolerze.

Drugi przycisk na głównym kontrolerze widoku powinien również być w stanie wyświetlać VC2 bezpośrednio z pominięciem VC1 i powinien wrócić do głównego kontrolera po kliknięciu przycisku VC2. Nie ma długo działającego kodu, pętli ani żadnych timerów. Tylko gołe połączenia kości, aby zobaczyć Kontrolery.

Author: Honey, 2013-02-16

6 answers

Ten wiersz:

[self dismissViewControllerAnimated:YES completion:nil];

Nie wysyła wiadomości do siebie, to w rzeczywistości wysyła wiadomość do swojego prezentującego VC, prosząc go o odrzucenie. Kiedy prezentujesz VC, tworzysz relację między prezentującym VC a prezentowanym. Nie powinieneś więc niszczyć prezentującego się VC podczas prezentacji (prezentowany VC nie może wysłać tej wiadomości z powrotem...). Ponieważ nie bierzesz tego pod uwagę, zostawiasz aplikację w stanie dezorientacji. Zobacz moją odpowiedź odrzucenie prezentowanego kontrolera widoku w którym polecam tę metodę jest wyraźniej napisane:

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

W Twoim przypadku musisz upewnić się, że cała kontrola odbywa się w mainVC. Powinieneś użyć delegata, aby wysłać poprawną wiadomość z powrotem do MainViewController z ViewController1, aby mainVC mógł odrzucić VC1, a następnie przedstawić VC2.

In VC2 VC1 dodaj protokół w swoim .plik h nad interfejsem@:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

I niżej w ten sam plik w sekcji @interface deklaruje właściwość do przechowywania wskaźnika delegata:

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

W VC1 .plik m, metoda przycisku Odrzuć 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 wdrożyć metodę delegowania:

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

present2: Może być tą samą metodą co metoda VC2Pressed: button IBAction. Zauważ, że jest wywoływany z bloku zakończenia, aby upewnić się, że VC2 nie jest prezentowany, dopóki VC1 jest całkowicie zwolniony.

Teraz przechodzisz z VC1->VCMain - > VC2, więc prawdopodobnie będziesz chciał, aby tylko jedno przejście było animowane.

Update

W swoich komentarzach wyrażasz zdziwienie złożonością wymaganą do osiągnięcia pozornie prostej rzeczy. Zapewniam cię, że ten wzór delegacji jest tak centralny dla wielu Objective-C i Cocoa, a ten przykład jest o najbardziej proste można uzyskać, że naprawdę należy podjąć wysiłek, aby uzyskać komfortowe z nim.

W Apple View Controller Programming Guide mają to powiedzieć:

Odrzucenie prezentowanego kontrolera widoku

Kiedy przychodzi czas na odrzucenie prezentowanego kontrolera widoku, preferowanym podejściem jest pozwolić, aby prezentujący kontroler go odrzucił. Innymi słowy, o ile to możliwe, ten sam kontroler widoku, który przedstawił kontroler widoku, powinien również wziąć odpowiedzialność za jego odrzucenie. Chociaż są kilka technik powiadamiania prezentującego kontrolera widoku, że jego prezentowany kontroler widoku 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 zamierzasz to osiągnąć, zdasz sobie sprawę, że wysyłanie wiadomości do MainViewController, aby wykonać całą pracę, jest jedynym logicznym wyjściem, biorąc pod uwagę, że nie chcesz używać NavigationController. Jeśli używasz NavController, w efekcie "delegujesz", nawet jeśli nie jawnie, do navController, aby wykonać całą pracę. Musi istnieć jakiś {52]} obiekt, który utrzymuje centralny ślad tego, co dzieje się z Twoją nawigacją VC, a ty potrzebujesz jakiegoś {52]} sposobu komunikowania się z nim, cokolwiek robisz.

[[20]}W praktyce porady Apple ' a są trochę ekstremalne... w normalnych przypadkach nie trzeba tworzyć dedykowanego delegata i metody, można możesz polegać na [self presentingViewController] dismissViewControllerAnimated: - to wtedy, gdy w takich przypadkach jak twoja chcesz, aby Twoje odrzucenie miało inny wpływ na odległe obiekty, o które musisz zadbać.

Oto coś, co możesz sobie wyobrazić do pracy bez kłopotów z delegowaniem...

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

}

Po poproszeniu prezentującego kontrolera o zwolnienie nas, mamy blok dopełniania, który wywołuje metodę w presentingViewController, aby wywołać VC2. Delegat nie jest potrzebny. (Dużą zaletą bloków jest to, że zmniejszają potrzeba delegatów w tych okolicznościach). Jednak w tym przypadku jest kilka rzeczy, które wchodzą w drogę...

  • 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ąć.
  • gdy VC1 zostanie odrzucony, nie ma możliwości wykonania bloku zakończenia... a może jest? Robi siebie.presentingViewController już coś znaczy? Nie wiesz (ja też nie wiem)... z delegatem nie masz tej niepewności.
  • kiedy próbuję uruchomić tę metodę, po prostu zawiesza się bez ostrzeżeń lub błędów.
Więc proszę... poświęć czas na naukę!

Update2

W Twoim komentarzu udało Ci się to zrobić, używając tego w obsłudze przycisku zwalniania VC2:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 
Jest to z pewnością dużo prostsze, ale pozostawia wiele problemów.

szczelne sprzęgło
Jesteś hard-okablowanie struktury viewController razem. Na przykład, jeśli wstawisz nowy kontroler viewController przed mainVC, Twoje wymagane zachowanie ulegnie złamaniu (przejdziesz do poprzedniego). W VC1 trzeba było również # import VC2. Dlatego masz dość dużo zależności między sobą, co łamie cele OOP/MVC.

Używając delegatów, ani VC1, ani VC2 nie muszą wiedzieć nic o mainVC, albo jest to poprzedzające, więc trzymamy wszystko luźno powiązane i modułowe.

pamięć
VC1 nie odszedł, nadal masz do niego dwa wskaźniki:

  • mainvc ' s presentedViewController property
  • VC2 ' s presentingViewController property
Możesz to przetestować, logując się, a także po prostu robiąc to z VC2.]}
[self dismissViewControllerAnimated:YES completion:nil]; 
To nadal działa, wciąż wraca do VC1. Wydaje mi się, że to wyciek pamięci. W tym przypadku nie jest to możliwe.]}
[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 rozkłada się, ponieważ próbujesz odrzucić prezentujący VC Z czego VC2 jest prezentowanym VC. Druga wiadomość tak naprawdę nie zostanie wykonana - cóż, być może niektóre rzeczy się zdarzają, ale nadal pozostajesz z dwoma wskaźnikami do obiektu, którego myślałeś, że się pozbyłeś. (edit-sprawdziłem to i nie jest tak źle, oba obiekty znikają po powrocie do mainVC)

To dość długi sposób mówienia-proszę, użyj delegatów. Jeśli to pomoże, zrobiłem kolejny. Krótki opis wzoru tutaj:
czy podanie kontrolera w konstruktorze jest zawsze złą praktyką?

Update 3
Jeśli naprawdę chcesz uniknąć delegatów, to może być najlepszym wyjściem: {]}

W VC1:

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

Ale nie odrzucaj niczego... jak ustaliliśmy, to i tak się nie dzieje.

W VC2:

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

Ponieważ (wiemy) nie oddaliliśmy VC1, możemy sięgnąć wstecz przez VC1 do MainVC. Mainvc1 Ponieważ VC1 zniknął, jest przedstawiony VC2 idzie z nim, więc jesteś z powrotem na MainVC w stanie czystym.

Nadal jest wysoce sprzężony, ponieważ VC1 musi wiedzieć o VC2, a VC2 musi wiedzieć, że został dostarczony przez MainVC - >VC1, ale to najlepsze, co dostaniesz bez odrobiny wyraźnej delegacji.

 185
Author: foundry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:46:50

Myślę, że źle zrozumiałeś niektóre podstawowe pojęcia dotyczące kontrolerów widoku modalnego iOS. Gdy odrzucisz VC1, wszystkie przedstawione Kontrolery widoku przez VC1 również zostaną odrzucone. Apple przeznaczone dla kontrolerów widoku modalnego do przepływu w sposób ułożony-w Twoim przypadku VC2 jest prezentowany przez VC1. Odrzucasz VC1 jak tylko przedstawisz VC2 z VC1 więc to totalny bałagan. Aby osiągnąć to, co chcesz, buttonPressedFromVC1 powinien mieć mainVC obecny VC2 natychmiast po odrzuceniu VC1. I myślę, że można to osiągnąć bez delegatów. Coś w tym stylu:

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

Zauważ, że ja.presentingViewController jest przechowywany w jakiejś innej zmiennej, ponieważ po odrzuceniu vc1 nie powinieneś się do niej odwoływać.

 9
Author: Radu Simionescu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-25 14:59:40

Przykład w Swift , przedstawiający Wyjaśnienie Odlewni powyżej i dokumentację Apple:

  1. na podstawie dokumentacji Apple ' a i odlewni Wyjaśnienie powyżej( poprawianie niektórych błędów), presentViewController wersja 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. na podstawie Odlewni Wyjaśnienie powyżej (poprawianie 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 {

}
 8
Author: King-Wizard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-12 01:29:56

Radu Simionescu-awesome work! a poniżej Twoje rozwiązanie dla miłośników 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
Author: chrisco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-16 10:14:32

Chciałam tego:

MapVC jest mapą w trybie pełnoekranowym.

Po naciśnięciu przycisku otwiera się PopupVC (Nie w trybie pełnoekranowym) nad mapą.

Kiedy nacisnę przycisk w PopupVC, powróci do MapVC, a następnie chcę wykonać viewdidappear.

Zrobiłem to:

MapVC.m: w akcji przycisku, segue programowo i ustawić 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, nowy własność

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

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}
 0
Author: Mer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-31 11:47:38

Rozwiązałem problem używając UINavigationController podczas prezentacji. W MainVC, podczas prezentacji VC1

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

W VC1, kiedy chciałbym pokazać VC2 i odrzucić VC1 w tym samym czasie( TYLKO jedną animację), mogę mieć animację push przez

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

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

self.dismiss(animated: true, completion: nil)
 0
Author: Duong Ngo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-02 17:21:25