Wykrywanie naciśnięcia przycisku „Wstecz” na pasku nawigacyjnym

135

Muszę wykonać pewne czynności, gdy przycisk Wstecz (powrót do poprzedniego ekranu, powrót do widoku rodzica) jest naciśnięty na pasku nawigacyjnym.

Czy jest jakaś metoda, którą mogę zaimplementować, aby złapać zdarzenie i uruchomić pewne akcje, aby wstrzymać i zapisać dane, zanim ekran zniknie?

ewok
źródło
1
Spójrz na rozwiązanie w tym wątku
Jiri Volejnik
Zrobiłem to w ten sposób, aby pokazać tutaj decyzję
Taras

Odpowiedzi:

316

AKTUALIZACJA: Według niektórych komentarzy, rozwiązanie w oryginalnej odpowiedzi nie wydaje się działać w niektórych scenariuszach w iOS 8+. Nie mogę zweryfikować, że tak jest bez dalszych szczegółów.

Dla tych z Was jednak w takiej sytuacji jest alternatywa. Wykrywanie, kiedy kontroler widoku jest otwierany, jest możliwe poprzez nadpisanie willMove(toParentViewController:). Podstawową ideą jest to, że kontroler widoku jest otwierany, gdy parentjest nil.

Więcej informacji można znaleźć w sekcji „Implementowanie kontrolera widoku kontenera” .


Od czasu iOS 5 stwierdziłem, że najłatwiejszym sposobem radzenia sobie z tą sytuacją jest użycie nowej metody - (BOOL)isMovingFromParentViewController:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController) {
    // Do your stuff here
  }
}

- (BOOL)isMovingFromParentViewController ma sens, gdy pchasz i umieszczasz kontrolery w stosie nawigacyjnym.

Jeśli jednak prezentujesz kontrolery widoku modalnego, powinieneś - (BOOL)isBeingDismissedzamiast tego użyć :

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isBeingDismissed) {
    // Do your stuff here
  }
}

Jak wspomniano w tym pytaniu , możesz połączyć obie właściwości:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController || self.isBeingDismissed) {
    // Do your stuff here
  }
}

Inne rozwiązania opierają się na istnieniu pliku UINavigationBar. Zamiast tego bardziej podoba mi się moje podejście, ponieważ oddziela wymagane zadania do wykonania od akcji, która wywołała zdarzenie, tj. Naciśnięcie przycisku Wstecz.

elitalon
źródło
Podoba mi się twoja odpowiedź. Ale dlaczego użyłeś wyrażenia „Self.isBeingDismed”? W moim przypadku stwierdzenia w „self.isBeingDismissed” nie są realizowane.
Rutvij Kotecha
3
self.isMovingFromParentViewControllerma wartość TRUE, gdy programowo wyświetlam stos nawigacji popToRootViewControllerAnimated- bez dotykania przycisku Wstecz. Czy powinienem zlekceważyć twoją odpowiedź? (temat mówi, że na pasku nawigacyjnym naciśnięto przycisk „wstecz”)
kas-kad
2
Świetna odpowiedź, bardzo dziękuję. W Swift użyłem:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini
1
Powinieneś to zrobić tylko w ciągu, -viewDidDisappear:ponieważ możliwe jest, że otrzymasz -viewWillDisappear:bez znaku -viewDidDisappear:(np. Kiedy zaczniesz przesuwać, aby odrzucić element kontrolera nawigacji, a następnie anulujesz to przesunięcie.
Heath Borders
3
Wygląda na to, że nie jest już niezawodnym rozwiązaniem. Działało, gdy pierwszy raz tego użyłem (był to iOS 10). Ale teraz przypadkowo stwierdziłem, że spokojnie przestał działać (iOS 11). Musiałem przejść na rozwiązanie „willMove (toParentViewController)”.
Vitalii
100

Podczas gdy viewWillAppear()i viewDidDisappear() wywoływane po dotknięciu przycisku Wstecz, są również wywoływane w innych przypadkach. Zobacz koniec odpowiedzi, aby uzyskać więcej informacji na ten temat.

Korzystanie z UIViewController.parent

Wykrywanie przycisku wstecz jest lepsze, gdy VC jest usuwany z jego elementu nadrzędnego (NavigationController) za pomocą willMoveToParentViewController(_:)LUBdidMoveToParentViewController()

Jeśli rodzic ma wartość zero, kontroler widoku jest zdejmowany ze stosu nawigacji i odrzucany. Jeśli rodzic nie jest nil, jest dodawany do stosu i prezentowany.

// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
     [super willMoveToParentViewController:parent];
    if (!parent){
       // The back button was pressed or interactive gesture used
    }
}


// Swift
override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent)
    if parent == nil {
        // The back button was pressed or interactive gesture used
    }
}

Zamień się willMovena didMovei check self.parent do pracy po kontroler widoku zostaje odrzucona.

Zatrzymanie zwolnienia

Zwróć uwagę, że sprawdzenie rodzica nie pozwala na „wstrzymanie” przejścia, jeśli musisz wykonać jakiś rodzaj asynchronicznego zapisu. Aby to zrobić, możesz zaimplementować następujące. Jedynym minusem jest to, że tracisz fantazyjny / animowany przycisk Wstecz w stylu iOS. Uważaj również tutaj na interaktywny gest machnięcia. Użyj poniższych, aby obsłużyć ten przypadek.

var backButton : UIBarButtonItem!

override func viewDidLoad() {
    super.viewDidLoad()

     // Disable the swipe to make sure you get your chance to save
     self.navigationController?.interactivePopGestureRecognizer.enabled = false

     // Replace the default back button
    self.navigationItem.setHidesBackButton(true, animated: false)
    self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
    self.navigationItem.leftBarButtonItem = backButton
}

// Then handle the button selection
func goBack() {
    // Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
    self.navigationItem.leftBarButtonItem = nil
    someData.saveInBackground { (success, error) -> Void in
        if success {
            self.navigationController?.popViewControllerAnimated(true)
            // Don't forget to re-enable the interactive gesture
            self.navigationController?.interactivePopGestureRecognizer.enabled = true
        }
        else {
            self.navigationItem.leftBarButtonItem = self.backButton
            // Handle the error
        }
    }
}


Więcej na widoku pojawi się / pojawiło się

Jeśli nie masz viewWillAppear viewDidDisappearproblemu, przeanalizujmy przykład. Załóżmy, że masz trzy kontrolery widoku:

  1. ListVC: Widok tabeli rzeczy
  2. DetailVC: Szczegóły dotyczące rzeczy
  3. SettingsVC: Niektóre opcje

Pozwala śledzić połączenia w detailVCmiarę przechodzenia od listVCdo settingsVCiz powrotemlistVC

Lista> Szczegóły (push szczegółyVC) Detail.viewDidAppear<- wyświetl
Szczegóły> Ustawienia (ustawienia pushVC ) Detail.viewDidDisappear<- znikają

A jak wrócimy ...
Ustawienia> Szczegóły (wyskakują ustawieniaVC) Detail.viewDidAppear<- pojawiają się
szczegóły> Lista (pojawiają się szczegółyVC) Detail.viewDidDisappear<- znikają

Zauważ, że viewDidDisappearjest to wywoływane wiele razy, nie tylko podczas cofania, ale także podczas jazdy do przodu. Dla szybkiej operacji, która może być pożądana, ale dla bardziej złożonej operacji, takiej jak połączenie sieciowe w celu zapisania, może nie być.

WCByrne
źródło
Tylko uwaga, użytkownik didMoveToParantViewController:ma wykonać pracę, gdy widok nie jest już widoczny. Pomocne dla iOS7 dzięki interaktywnemu
Gesutre
didMoveToParentViewController * jest literówka
thewormsterror
Nie zapomnij wywołać [super willMoveToParentViewController: parent]!
ScottyB
2
Parametr parent ma wartość nil, gdy przechodzisz do kontrolera widoku nadrzędnego i niezerową, gdy widok, w którym pojawia się ta metoda, jest wyświetlany. Możesz użyć tego faktu, aby wykonać akcję tylko po naciśnięciu przycisku Wstecz, a nie po przejściu do widoku. W końcu to było pierwotne pytanie. :)
Mike
1
Jest to również wywoływane, gdy używa się _ = self.navigationController?.popViewController(animated: true)go programowo , więc nie jest wywoływane tylko po naciśnięciu przycisku Wstecz. Szukam połączenia, które działa tylko po naciśnięciu przycisku Wstecz.
Ethan Allen
16

Pierwsza metoda

- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (![parent isEqual:self.parentViewController]) {
         NSLog(@"Back pressed");
    }
}

Druga metoda

-(void) viewWillDisappear:(BOOL)animated {
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
       // back button was pressed.  We know this is true because self is no longer
       // in the navigation stack.  
    }
    [super viewWillDisappear:animated];
}
Zar E Ahmer
źródło
1
Druga metoda była jedyną, która działała dla mnie. Pierwsza metoda została również wywołana po przedstawieniu mojego poglądu, co nie było akceptowalne w moim przypadku użycia.
marcshilling
10

Mylą się ci, którzy twierdzą, że to nie działa:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if self.isMovingFromParent {
        print("we are being popped")
    }
}

To działa dobrze. Więc co jest przyczyną rozpowszechnienia się mitu, że tak nie jest?

Problem wydaje się wynikać z nieprawidłowej implementacji innej metody, a mianowicie z tego, że implementacja willMove(toParent:)zapomniała zadzwonić super.

Jeśli zaimplementujesz willMove(toParent:)bez wywołania super, to self.isMovingFromParentbędzie falsei użycie viewWillDisappearbędzie wyglądało na niepowodzenie. Nie zawiodło; zepsułeś to.

UWAGA: Prawdziwym problemem jest zwykle to, że drugi kontroler widoku wykrył, że pierwszy kontroler widoku został włamany. Zobacz także bardziej ogólną dyskusję tutaj: Unified UIViewController "stał się pierwszym" wykrywaniem?

EDYCJA Komentarz sugeruje, że powinno to być viewDidDisappearraczej niż viewWillDisappear.

matowe
źródło
Ten kod jest wykonywany po dotknięciu przycisku Wstecz, ale jest również wykonywany, jeśli VC jest programowo otwierany.
biomiker
@biomiker Jasne, ale dotyczyłoby to również innych podejść. Popping to popping. Pytanie brzmi, jak wykryć pop, gdy nie wyskoczyłeś programowo. Jeśli wyskakujesz programowo, to już wiesz, że wyskakujesz, więc nie ma nic do wykrycia.
mat.
Tak, dotyczy to kilku innych podejść i wiele z nich ma podobne komentarze. Po prostu wyjaśniłem, ponieważ była to niedawna odpowiedź z konkretnym obaleniem i nabrałem nadziei, kiedy ją przeczytałem. Dla przypomnienia, pytanie brzmi, jak wykryć naciśnięcie przycisku Wstecz. Rozsądnym argumentem jest stwierdzenie, że kod, który będzie wykonywany również w sytuacjach, gdy przycisk Wstecz nie zostanie naciśnięty, bez wskazania, czy przycisk Wstecz został naciśnięty, czy nie, nie rozwiązuje w pełni prawdziwego pytania, nawet jeśli być może pytanie mogło być bardziej wyraźnie w tej kwestii.
biomiker
1
Niestety, powraca to truedla interaktywnego gestu machnięcia pop - z lewej krawędzi kontrolera widoku - nawet jeśli przesunięcie nie spowodowało całkowitego wyskoczenia. Więc zamiast sprawdzać to willDisappearw didDisappearpracy.
badhanganesh
1
@badhanganesh Dzięki, zredagowano odpowiedź, tak aby zawierała te informacje.
mat.
9

Gram (lub walczę) z tym problemem przez dwa dni. IMO najlepszym podejściem jest po prostu utworzenie klasy rozszerzenia i protokołu, na przykład:

@protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
 * Indicates that the back button was pressed.
 * If this message is implemented the pop logic must be manually handled.
 */
- (void)backButtonPressed;
@end

@interface UINavigationController(BackButtonHandler)
@end

@implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
    SEL backButtonPressedSel = @selector(backButtonPressed);
    if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
        [topViewController performSelector:backButtonPressedSel];
        return NO;
    }
    else {
        [self popViewControllerAnimated:YES];
        return YES;
    }
}
@end

To działa, ponieważ UINavigationControllerotrzyma wywołanie za navigationBar:shouldPopItem:każdym razem, gdy zostanie wyświetlony kontroler widoku. Tam wykrywamy, czy przycisk Back został naciśnięty, czy nie (inny przycisk). Jedyne, co musisz zrobić, to zaimplementować protokół w kontrolerze widoku, w którym naciśnięto przycisk wstecz.

Pamiętaj, aby ręcznie włożyć kontroler widoku do środka backButtonPressedSel, jeśli wszystko jest w porządku.

Jeśli masz już podklasę UINavigationViewControlleri zaimplementowałeś ją navigationBar:shouldPopItem:, nie martw się, to nie będzie ci przeszkadzać.

Możesz być także zainteresowany wyłączeniem gestu cofania.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
7ynk3r
źródło
1
Ta odpowiedź była dla mnie prawie kompletna, z wyjątkiem tego, że stwierdziłem, że często wyskakują 2 kontrolery widoku. Zwrócenie TAK powoduje, że metoda wywołująca wywołuje pop, więc wywołanie pop również oznaczało, że zostaną pobrane 2 kontrolery widoku. Zobacz tę odpowiedź na inne pytanie, aby uzyskać więcej deetów (bardzo dobra odpowiedź, która zasługuje na więcej głosów): stackoverflow.com/a/26084150/978083
Jason Ridge
Słuszna uwaga, mój opis nie był jasny co do tego faktu. Opcja „Pamiętaj, aby ręcznie otworzyć kontroler widoku, jeśli wszystko jest w porządku” dotyczy tylko przypadku zwrócenia „NIE”, w przeciwnym razie przepływ jest normalnym pop.
7ynk3r
1
W przypadku gałęzi „else” lepiej jest wywołać super implementację, jeśli nie chcesz samodzielnie obsługiwać popu i pozwolić mu zwracać to, co uważa za właściwe, co jest przeważnie TAK, ale także sam dba o pop i odpowiednio animuje szewron .
Ben Sinclair
9

Działa to dla mnie w iOS 9.3.x z Swift:

override func didMoveToParentViewController(parent: UIViewController?) {
    super.didMoveToParentViewController(parent)

    if parent == self.navigationController?.parentViewController {
        print("Back tapped")
    }
}

W przeciwieństwie do innych rozwiązań tutaj, nie wydaje się to wywoływać nieoczekiwanie.

Chris Villa
źródło
lepiej zamiast tego użyć willMove
Eugene Gordin,
4

Dla przypomnienia, myślę, że tego szukał bardziej…

    UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(backToRootView:)];

    self.navigationItem.leftBarButtonItem = l_backButton;


    - (void) backToRootView:(id)sender {

        // Perform some custom code

        [self.navigationController popToRootViewControllerAnimated:YES];
    }
Paul Brady
źródło
1
Dzięki Paul, to rozwiązanie jest dość proste. Niestety ikona jest inna. To jest ikona „przewijania”, a nie ikona wstecz. Może jest sposób na użycie ikony wstecznej ...
Ferran Maylinch
2

Jak purrrminatormówi, odpowiedź przez elitalonnie jest całkowicie poprawna, ponieważ your stuffzostanie wykonana nawet podczas programowego wyskakiwania kontrolera.

Rozwiązanie, które znalazłem do tej pory nie jest zbyt ładne, ale na mnie działa. Poza tym, co elitalonpowiedziałem, sprawdzam też, czy wyskakuję programowo, czy nie:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

Musisz dodać tę właściwość do swojego kontrolera i ustawić ją na TAK przed programowym pojawieniem się:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

Dzięki za pomoc!

Ferran Maylinch
źródło
2

Najlepszym sposobem jest użycie metod delegata UINavigationController

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

Dzięki temu można dowiedzieć się, jaki kontroler wyświetla UINavigationController.

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}
Harald
źródło
To powinno być oznaczone jako poprawna odpowiedź! Może również zechcesz dodać jeszcze jedną linię, aby przypomnieć ludziom -> self.navigationController.delegate = self;
Mike Critchley
2

Rozwiązałem ten problem, dodając UIControl do paska nawigacji po lewej stronie.

UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:@selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];

I musisz pamiętać, aby go usunąć, gdy zniknie widok:

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.leftItemControl) {
        [self.leftItemControl removeFromSuperview];
    }    
}

To wszystko!

Eric
źródło
2

Możesz skorzystać z wywołania zwrotnego przycisku Wstecz, na przykład:

- (BOOL) navigationShouldPopOnBackButton
{
    [self backAction];
    return NO;
}

- (void) backAction {
    // your code goes here
    // show confirmation alert, for example
    // ...
}

dla szybkiej wersji możesz zrobić coś podobnego w zakresie globalnym

extension UIViewController {
     @objc func navigationShouldPopOnBackButton() -> Bool {
     return true
    }
}

extension UINavigationController: UINavigationBarDelegate {
     public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
          return self.topViewController?.navigationShouldPopOnBackButton() ?? true
    }
}

Poniżej umieszczasz kontroler widoku, w którym chcesz sterować działaniem przycisku Wstecz:

override func navigationShouldPopOnBackButton() -> Bool {
    self.backAction()//Your action you want to perform.

    return true
}
Pedro Magalhães
źródło
1
Nie wiem, dlaczego ktoś przegłosował. Wydaje się, że jest to zdecydowanie najlepsza odpowiedź.
Avinash
@Avinash Skąd się navigationShouldPopOnBackButtonbierze? Nie jest częścią publicznego interfejsu API.
elitalon
@elitalon Przepraszamy, to była połowa odpowiedzi. Myślałem, że pozostały kontekst jest tutaj wątpliwy. W każdym razie zaktualizowałem odpowiedź teraz
Avinash
1

Jak powiedział Coli88, powinieneś sprawdzić protokół UINavigationBarDelegate.

Mówiąc bardziej ogólnie, możesz również użyć - (void)viewWillDisapear:(BOOL)animateddo wykonania niestandardowej pracy, gdy widok zachowany przez aktualnie widoczny kontroler widoku wkrótce zniknie. Niestety, obejmowałoby to kłopotliwe przypadki push i pop.

ramdam
źródło
1

Dla Swift z kontrolerem UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
źródło
1

Odpowiedź 7ynk3r była bardzo zbliżona do tego, czego użyłem na końcu, ale wymagała pewnych poprawek:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;

    if (wasBackButtonClicked) {
        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
            // if user did press back on the view controller where you handle the navBackButtonPressed
            [topViewController performSelector:@selector(navBackButtonPressed)];
            return NO;
        } else {
            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
            [self popViewControllerAnimated:YES];
            return YES;
        }
    } else {
        // when you call popViewController programmatically you do not want to pop it twice
        return YES;
    }
}
micromanc3r
źródło
0

self.navigationController.isMovingFromParentViewController nie działa już na iOS8 i 9 używam:

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}
Wasilij
źródło
-1

(SZYBKI)

ostatecznie znalazłem rozwiązanie .. szukaną metodą jest "willShowViewController", która jest metodą delegowaną dla UINavigationController

//IMPORT UINavigationControllerDelegate !!
class PushedController: UIViewController, UINavigationControllerDelegate {

    override func viewDidLoad() {
        //set delegate to current class (self)
        navigationController?.delegate = self
    }

    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
        //MyViewController shoud be the name of your parent Class
        if var myViewController = viewController as? MyViewController {
            //YOUR STUFF
        }
    }
}
Jiří Zahálka
źródło
1
Problem z tym podejściem polega na tym, że łączy się MyViewControllerz nim PushedController.
clozach