Wykryj, kiedy prezentowany kontroler widoku zostanie odrzucony

82

Powiedzmy, że mam instancję klasy kontrolera widoku o nazwie VC2. W VC2 jest przycisk „Anuluj”, który sam się zamyka. Ale nie mogę wykryć ani odebrać żadnego oddzwonienia, gdy przycisk „anuluj” został wyzwolony. VC2 to czarna skrzynka.

Kontroler widoku (zwany VC1) przedstawi VC2 przy użyciu presentViewController:animated:completion:metody.

Jakie opcje musi wykryć VC1, kiedy VC2 został odrzucony?

Edycja: Z komentarza @rory mckinnel i odpowiedzi @NicolasMiari próbowałem następujących rzeczy:

W VC2:

-(void)cancelButton:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:^{

    }];
//    [super dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

W VC1:

//-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
- (void)dismissViewControllerAnimated:(BOOL)flag
                           completion:(void (^ _Nullable)(void))completion
{
    NSLog(@"%s ", __PRETTY_FUNCTION__);
    [super dismissViewControllerAnimated:flag completion:completion];
//    [self dismissViewControllerAnimated:YES completion:^{
//        
//    }];
}

Ale dismissViewControllerAnimatedw VC1 nie dzwoniono.

user523234
źródło
2
w VC1 zostanie wywołana metoda viewWillAppear
Istvan
1
Według dokumentów kontroler przedstawiający jest odpowiedzialny za faktyczne zwolnienie. Kiedy przedstawiony kontroler zwolni się, poprosi prezentera, aby zrobił to za niego. Więc jeśli nadpisujesz dismissViewControllerAnimatedw kontrolerze VC1, wierzę, że zostanie on wywołany, gdy naciśniesz anuluj na VC2. Wykryj odrzucenie, a następnie wywołaj wersję superklas, która wykona faktyczne odrzucenie.
Rory McKinnel
1
Możesz sprawdzić swoje sterowanie, dzwoniąc [self.presentingViewController dismissViewControllerAnimated]. Może się zdarzyć, że kod wewnętrzny ma inny mechanizm poproszenia prezentera o zwolnienie.
Rory McKinnel,
@RoryMcKinnel: Używanie self.presentingViewController działało w moim laboratorium VC2, a także z prawdziwej czarnej skrzynki. Jeśli umieścisz swoje uwagi w odpowiedzi, wybiorę je jako odpowiedź. Dzięki.
user523234
Rozwiązanie tego problemu można znaleźć w tym pokrewnym poście: stackoverflow.com/a/34571641/3643020
Campbell_Souped

Odpowiedzi:

64

Zgodnie z dokumentacją za faktyczne zwolnienie odpowiedzialny jest kontroler przedstawiający. Kiedy przedstawiony kontroler zwolni się, poprosi prezentera, aby zrobił to za niego. Więc jeśli zastąpisz dismissViewControllerAnimated w kontrolerze VC1, wierzę, że zostanie wywołany po naciśnięciu przycisku Cancel na VC2. Wykryj odrzucenie, a następnie wywołaj wersję superklas, która wykona faktyczne odrzucenie.

Jak wynika z dyskusji, nie wydaje się to działać. Zamiast polegać na mechanizmu bazowego, zamiast dzwonić dismissViewControllerAnimated:completionna samej VC2, zadzwoń dismissViewControllerAnimated:completionnaself.presentingViewController w VC2. Spowoduje to bezpośrednie wywołanie nadpisania.

Całkowicie lepszym podejściem byłoby posiadanie VC2 zapewniającego blok, który jest wywoływany po zakończeniu działania kontrolera modalnego.

Tak więc w VC2 podaj właściwość bloku powiedz z nazwą onDoneBlock .

W VC1 prezentujesz w następujący sposób:

  • W VC1 utwórz VC2

  • Ustaw gotową obsługę dla VC2 jako: VC2.onDoneBlock={[VC2 dismissViewControllerAnimated:YES completion:nil]};

  • Przedstaw kontroler VC2 w normalny sposób, używając [self presentViewController: VC2 animated: YES complete: nil];

  • W VC2 w wywołaniu akcji Cancel target self.onDoneBlock();

W rezultacie VC2 mówi każdemu, kto go podniesie, że jest zrobione. Możesz rozszerzyć, onDoneBlockaby mieć argumenty, które wskazują, czy modalne zostało zakończone, anulowane, powiodło się itp.

Rory McKinnel
źródło
2
Chce tylko podziękować i docenić, jak pięknie to działa ... nawet po 4 latach! Dziękuję Ci!
Anna
48

Wewnątrz znajduje się specjalna właściwość logiczna o UIViewControllernazwie isBeingDismissed, której możesz użyć do tego celu:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if isBeingDismissed {
        // TODO: Do your stuff here.
    }
}
Joris Weimar
źródło
3
Najłatwiejsza najlepsza odpowiedź, poprawnie rozwiązuje większość problemów i nie wymaga dodatkowych implementacji.
wydany
Nie działa poprawnie bez sparowania z viewDidAppear.
Dmitry
9
W prezentacji modalnej iOS13 będzie to prawdą, gdy użytkownik zacznie przeciągać kontroler, aby odrzucić, ale może zdecydować, że nie dokończy zwolnienia.
Estel
44

Użyj właściwości bloku

Zadeklaruj w VC2

var onDoneBlock : ((Bool) -> Void)?

Konfiguracja w VC1

VC2.onDoneBlock = { result in
                // Do something
            }

Zadzwoń do VC2, kiedy masz zamiar zwolnić

onDoneBlock!(true)
brycejl
źródło
@ Bryce64 To nie działa dla mnie, otrzymałem „Wątek 1: Błąd krytyczny: nieoczekiwanie znaleziono zero podczas rozpakowywania wartości opcjonalnej” w momencie, gdy kod przechodzi do onDoneBlock! (Prawda)
Lucas
@Lucas Wygląda na to, że nie zadeklarowałeś go poprawnie w VC1. Znak „!” wymusza rozpakowanie, aby wymusić błąd, jeśli nie masz go poprawnie skonfigurowanego.
brycejl
1
Zakłada się, że prezentowany jest tylko jeden kontroler widoku. Możesz być na stosie nawigacji, Bóg wie gdzie.
Lee Probert,
@LeeProbert Dokładnie. Mamy prezentowany kontroler nawigacji z około 10 możliwymi kontrolerami podrzędnymi na swoim stosie i prawie każdy z nich może wywołać odrzucenie ... w takiej sytuacji każdy blok uzupełniania musiałby zostać przekazany do wszystkich 10 takich kontrolerów
Igor Vasilev
13

Może wywoływać zarówno kontroler prezentujący, jak i prezentowany widokdismissViewController:animated: w celu odrzucenia kontrolera widoku prezentowanego.

Pierwsza opcja jest (prawdopodobnie) „poprawna”, z punktu widzenia projektu: ten sam „nadrzędny” kontroler widoku jest odpowiedzialny zarówno za prezentowanie, jak i odrzucanie modalnego („podrzędnego”) kontrolera widoku.

Jednak to drugie jest wygodniejsze: zazwyczaj przycisk „odrzuć” jest dołączony do widoku kontrolera widoku prezentowanego i ma wspomniany kontroler widoku ustawiony jako cel akcji.

Jeśli stosujesz poprzednie podejście, znasz już wiersz kodu w kontrolerze widoku prezentacji, w którym występuje odrzucenie: albo uruchom kod zaraz po dismissViewControllerAnimated:completion: , albo w bloku uzupełniania.

Jeśli przyjmujesz to drugie podejście (kontroler widoku prezentowanego odrzuca się), pamiętaj, że wywołanie dismissViewControllerAnimated:completion:kontrolera widoku prezentowanego powoduje, że UIKit z kolei wywołuje tę metodę na kontrolerze widoku prezentacyjnego:

Dyskusja

Kontroler widoku prezentacji jest odpowiedzialny za odrzucenie kontrolera widoku, który przedstawił. Jeśli wywołasz tę metodę na samym kontrolerze prezentowanego widoku, UIKit poprosi kontroler widoku prezentacji o obsługę odrzucenia.

( źródło: odwołanie do klasy UIViewController )

Aby więc przechwycić takie zdarzenie, możesz nadpisać tę metodę w kontrolerze widoku prezentującego :

override func dismiss(animated flag: Bool,
                         completion: (() -> Void)?) {
    super.dismiss(animated: flag, completion: completion)

    // Your custom code here...
}
Nicolas Miari
źródło
1
Nie ma problemu. Ale okazuje się, że nie działa zgodnie z oczekiwaniami. Na szczęście odpowiedź @ RoryMcKinnel wydaje się dawać więcej opcji.
Nicolas Miari
Chociaż to podejście jest wystarczające do podklasowania kontrolera widoku z kontrolera widoku podstawowego, zastępującego discissViewControllerAnimated w tym. Ale to się nie udaje, jeśli spróbujesz zamknąć kontroler widoku w kontrolerze widoku nawigacji
hariszaman
4
Nie jest wywoływana, gdy użytkownik odrzuca kontroler widoku modalnego, przesuwając palcem od góry!
Dmitry
3
extension Foo: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        //call whatever you want
    }
}

vc.presentationController?.delegate = foo
Ivan
źródło
1
iOS 13.0+tylko
gondo
2

Aby wykonać to zadanie, możesz użyć odprężania segue, bez konieczności używania discissModalViewController. Zdefiniuj metodę odwijania płynności w swoim VC1.

Zobacz ten link, aby dowiedzieć się, jak utworzyć odprężającą ścieżkę, https://stackoverflow.com/a/15839298/5647055 .

Zakładając, że Twoja odprężająca ścieżka jest skonfigurowana, w metodzie działania zdefiniowanej dla przycisku „Anuluj”, możesz wykonać przejście jako -

[self performSegueWithIdentifier:@"YourUnwindSegueName" sender:nil];

Teraz, gdy naciśniesz przycisk „Anuluj” w VC2, zostanie on odrzucony i pojawi się VC1. Wywoła również metodę odwijania, zdefiniowaną w VC1. Teraz wiesz, kiedy przedstawiony kontroler widoku zostanie odrzucony.

Valar Morghulis
źródło
2

Używam następujących znaków, aby zasygnalizować koordynatorowi, że kontroler widoku jest „gotowy”. Jest to używane w AVPlayerViewControllerpodklasie w aplikacji tvOS i zostanie wywołane po zakończeniu przejścia od zwolnienia playerVC:

class PlayerViewController: AVPlayerViewController {
  var onDismissal: (() -> Void)?

  override func beginAppearanceTransition(_ isAppearing: Bool, animated: Bool) {
    super.beginAppearanceTransition(isAppearing, animated: animated)
    transitionCoordinator?.animate(alongsideTransition: nil,
      completion: { [weak self] _ in
         if !isAppearing {
            self?.onDismissal?()
        }
    })
  }
}
fruitcoder
źródło
Nie powinieneś dziedziczyć po AVPLayerViewController. Dokumentacja firmy Apple mówi: „Tworzenie podklas AVPlayerViewController i zastępowanie jego metod nie jest obsługiwane i powoduje niezdefiniowane zachowanie”.
Neru
2

Korzystanie z willMove(toParent: UIViewController?)poniższego sposobu wydawało mi się działać. (Testowane na iOS12).

override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent);

    if parent == nil
    {
        // View controller is being removed.
        // Perform onDismiss action
    }
}
Shavi
źródło
1

@ user523234 - "Ale nie wywoływano pliku dismissViewControllerAnimated w VC1."

Nie możesz założyć, że VC1 faktycznie przeprowadza prezentację - może to być główny kontroler widoku, powiedzmy VC0. W grę wchodzą 3 kontrolery widoku:

  • sourceViewController
  • PresentingViewController
  • presentViewController

W przykładzie VC1 = sourceViewController, VC2 = presentedViewController,?? = presentingViewController - może VC1, a może nie.

Jednak zawsze możesz polegać na wywołaniu VC1.animationControllerForDismissedController (jeśli zaimplementowałeś metody delegata) podczas odrzucania VC2 iw tej metodzie możesz robić, co chcesz z VC1

Andrew Coad
źródło
1

Widziałem ten post tyle razy, gdy zajmowałem się tym problemem, pomyślałem, że w końcu mogę rzucić trochę światła na możliwą odpowiedź.

Jeśli potrzebujesz wiedzieć, czy akcje inicjowane przez użytkownika (takie jak gesty na ekranie) spowodowały zwolnienie kontrolera UIActionController i nie chcesz poświęcać czasu na tworzenie podklas, rozszerzeń lub czegokolwiek w kodzie, istnieje alternatywa.

Jak się okazuje, właściwość popoverPresentationController elementu UIActionController (lub raczej dowolnego UIViewController w tym celu) ma delegata, który można ustawić w dowolnym momencie w kodzie, który jest typu UIPopoverPresentationControllerDelegate i ma następujące metody:

Przypisz delegata ze swojego kontrolera akcji, zaimplementuj wybrane metody w klasie delegatów (widok, kontroler widoku lub cokolwiek) i voila!

Mam nadzieję że to pomoże.

Izhido
źródło
A te są przestarzałe od iOS 13. Doh
Ben Affleck
1

Inną opcją jest odsłuchanie zwolnieniaTransitionDidEnd () niestandardowego kontrolera UIPresentationController

Igor Vasilev
źródło
0
  1. Utwórz jeden plik klasy (.h / .m) i nazwij go: DismissSegue
  2. Wybierz podklasę: UIStoryboardSegue

  3. Przejdź do pliku DismissSegue.m i zapisz następujący kod:

    - (void)perform {
        UIViewController *sourceViewController = self.sourceViewController;
        [sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
    
  4. Otwórz scenorys, a następnie Ctrl + przeciągnij z przycisku anulowania do VC1 i wybierz Segment akcji jako Odrzuć i gotowe.

Tapansinh Solanki
źródło
0

Jeśli zastąpisz ukrywanie kontrolera widoku:

override func removeFromParentViewController() {
    super.removeFromParentViewController()
    // your code here
}

Przynajmniej to zadziałało dla mnie.

mxcl
źródło
@JohnScalo nie jest prawdą, sporo z „natywnych” hierarchii kontrolerów widoku implementuje się z prymitywami dziecko / rodzic.
mxcl
0

overrideing viewDidAppearzałatwił sprawę za mnie. Użyłem Singletona w moim modalu i jestem teraz w stanie ustawić i pobrać z tego w wywołującym VC, modalnym i wszędzie indziej.

W
źródło
viewDidAppear?
Dmitry
1
Czy chodziło Ci o viewDidDisappear?
Dale
0

viewWillDisappearFunkcja nadpisania w kontrolerze widoku prezentowanego.

override func viewWillDisappear(_ animated: Bool) {
    //Your code here
}
priwiljay
źródło
Nie działa poprawnie bez sparowania z viewDidAppear.
Dmitry
1
Koniecznie zadzwoń do super.viewWillDisappear
Dale,
0

Jak już wspomniano, rozwiązaniem jest użycie override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).

Dla tych, którzy zastanawiają się, dlaczego override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil)nie zawsze wydaje się działać, może się okazać, że połączenie jest przechwytywane przez a, UINavigationControllerjeśli jest zarządzane. Napisałem podklasę, która powinna pomóc:

class DismissingNavigationController: UINavigationController { override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { super.dismiss(animated: flag, completion: completion) topViewController?.dismiss(animated: flag, completion: completion) } }

Steve
źródło
0

Jeśli chcesz obsłużyć odrzucanie kontrolera widoku, powinieneś użyć poniższego kodu.

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.isBeingDismissed && self.completion != NULL) {
        self.completion();
    }
}

Niestety nie możemy wywołać zakończenia w metodzie przesłoniętej - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion;ponieważ ta metoda jest wywoływana tylko wtedy, gdy wywołujesz metodę odrzucenia tego kontrolera widoku.

Arthur Kvaratskhelija
źródło
Ale viewWillDisappearteż nie działa poprawnie bez sparowania z viewDidAppear.
Dmitry
viewWillDisappear jest wywoływane, gdy VC jest całkowicie zakryte (np. modalem). Być może nie zostałeś zwolniony
Lou Franco
0

Użyłem deinit dla ViewController

deinit {
    dataSource.stopUpdates()
}

Deinitializer jest wywoływany bezpośrednio przed cofnięciem przydziału instancji klasy.

Dan Alboteanu
źródło