Обнаружение, когда кнопка «назад» нажата на панели навигации


135

Мне нужно выполнить некоторые действия, когда на навигационной панели нажата кнопка «Назад» (возврат к предыдущему экрану, возврат к родительскому виду).

Есть ли какой-нибудь метод, который я могу реализовать, чтобы перехватить событие и запустить некоторые действия, чтобы приостановить и сохранить данные до исчезновения экрана?




Я сделал это таким образом, чтобы показать решение здесь
Тарас

Ответы:


316

ОБНОВЛЕНИЕ: Согласно некоторым комментариям, решение в первоначальном ответе, похоже, не работает при определенных сценариях в iOS 8+. Я не могу подтвердить, что это действительно так, без дальнейших подробностей.

Для тех из вас, однако, в этой ситуации есть альтернатива. Обнаружение, когда контроллер представления выталкивается, возможно путем переопределения willMove(toParentViewController:). Основная идея заключается в том, что контроллер представления выталкивается, когда он parentесть nil.

Проверьте "Реализация Контроллера Контейнера Представления" для получения дальнейшей информации.


Начиная с iOS 5 я обнаружил, что самый простой способ справиться с этой ситуацией - использовать новый метод - (BOOL)isMovingFromParentViewController:

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

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

- (BOOL)isMovingFromParentViewController имеет смысл, когда вы нажимаете и выталкиваете контроллеры в стеке навигации.

Однако, если вы представляете контроллеры модального представления, вы должны использовать - (BOOL)isBeingDismissedвместо этого:

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

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

Как отмечено в этом вопросе , вы можете объединить оба свойства:

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

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

Другие решения основаны на существовании UINavigationBar. Вместо этого мне больше нравится мой подход, потому что он отделяет требуемые задачи от действия, которое вызвало событие, то есть нажатие кнопки «назад».


Мне нравится, ты отвечаешь. Но почему вы использовали «self.isBeingDismissed»? В моем случае операторы в self.isBeingDismissed не реализуются.
Рутвий Котеча

3
self.isMovingFromParentViewControllerимеет значение ИСТИНА, когда я загружаю стек навигации программно, popToRootViewControllerAnimatedбез какого-либо прикосновения к кнопке возврата. Должен ли я снизить ваш ответ? (субъект говорит, что на навигационной панели нажата кнопка «назад»)
kas-kad

2
Потрясающий ответ, большое спасибо. В Swift я использовал:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini

1
Вы должны сделать это только в пределах , -viewDidDisappear:так как вполне возможно , что вы получите -viewWillDisappear:без -viewDidDisappear:(например , когда вы начинаете ударяя отклонить элемент навигации контроллера , а затем отменить , что красть.
Heath Borders

3
Похоже, не надежное решение больше. Работал в то время, когда я впервые использовал это (это была iOS 10). Но теперь я случайно обнаружил, что он спокойно перестал работать (iOS 11). Пришлось переключиться на решение "willMove (toParentViewController)".
Виталий

100

Хотя viewWillAppear()и viewDidDisappear() вызываются, когда нажата кнопка «Назад», они также вызываются в другое время. Смотрите конец ответа для более подробной информации.

Использование UIViewController.parent

Обнаружение кнопки возврата лучше сделать, когда VC удален из родительского элемента (NavigationController) с помощью willMoveToParentViewController(_:)ИЛИdidMoveToParentViewController()

Если parent равен nil, контроллер представления извлекается из стека навигации и удаляется. Если parent не равен nil, он добавляется в стек и представляется.

// 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
    }
}

Выгрузить willMoveдля didMoveи проверки self.parent сделать работу после того, как контроллер зрения уволен.

Остановка увольнения

Обратите внимание, что проверка родителя не позволяет вам «приостановить» переход, если вам нужно выполнить какое-то асинхронное сохранение. Для этого вы можете реализовать следующее. Единственный недостаток - вы теряете красивую анимированную кнопку возврата в стиле iOS. Также будьте осторожны с интерактивным жестом. Используйте следующие для обработки этого случая.

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


Больше на вид будет / действительно появится

Если viewWillAppear viewDidDisappearпроблема не возникла, давайте рассмотрим пример. Скажем, у вас есть три контроллера вида:

  1. ListVC: табличное представление вещей
  2. DetailVC: подробности о вещи
  3. Настройки ВК: некоторые варианты вещи

Давайте следовать звонки на , detailVCкак вы идете от listVCк settingsVCи обратноlistVC

Список> Детализация (push detailVC) Detail.viewDidAppear<- появляется
Деталь> Настройки (push settingsVC) Detail.viewDidDisappear<- исчезает

И как мы вернемся ...
Настройки> Подробно (всплывающие настройки ВК) <- Detail.viewDidAppearПоявляются
подробности> Список (всплывающие подробности ВК) Detail.viewDidDisappear<- исчезают

Обратите внимание, что viewDidDisappearвызывается несколько раз, не только при движении назад, но и при движении вперед. Для быстрой операции, которая может быть желательной, но для более сложной операции, такой как сетевой вызов, для сохранения, это может не сработать.


Просто примечание, пользователь didMoveToParantViewController:может выполнять работу, когда представление больше не видно. Полезно для iOS7 с интерактивным
Gesutre

didMoveToParentViewController * есть опечатка
thewormsterror

Не забудьте вызвать [super willMoveToParentViewController: parent]!
ScottyB

2
Родительский параметр равен nil, когда вы подключаетесь к родительскому контроллеру представления, и не равен nil, когда отображается представление, в котором отображается этот метод. Вы можете использовать этот факт для выполнения действия только при нажатии кнопки «Назад», а не при достижении вида. Это был, в конце концов, оригинальный вопрос. :)
Майк

1
Это также вызывается при программном использовании _ = self.navigationController?.popViewController(animated: true), поэтому оно вызывается не только нажатием кнопки «Назад». Я ищу звонок, который работает только при нажатии Назад.
Итан Аллен

16

Первый метод

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

Второй метод

-(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];
}

1
Второй метод был единственным, который работал для меня. Первый метод также был вызван при представлении моего представления, что было неприемлемо для моего варианта использования.
marcshilling

10

Те, кто утверждает, что это не работает, ошибаются:

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

Это отлично работает. Так что же вызывает широко распространенный миф о том, что это не так?

Кажется, проблема связана с неправильной реализацией другого метода, а именно с тем, что реализация willMove(toParent:)забыла вызвать super.

Если вы реализуете willMove(toParent:)без вызова super, то self.isMovingFromParentбудет falseи использование viewWillDisappearбудет не в состоянии. Это не подвело; ты сломал это.

ПРИМЕЧАНИЕ . Настоящая проблема обычно заключается во втором контроллере вида, обнаружившем, что первый контроллер вида отключен. Пожалуйста, см. Также более общее обсуждение здесь: Unified UIViewController "стал самым передовым" обнаружением?

РЕДАКТИРОВАТЬ Комментарий предполагает, что это должно быть, viewDidDisappearа не viewWillDisappear.


Этот код выполняется, когда нажимается кнопка «Назад», но также выполняется, если VC запускается программно.
biomiker

@biomiker Конечно, но это также относится и к другим подходам. Поппинг лопается. Вопрос в том, как обнаружить поп-музыку, если вы не работали программно. Если вы поп-программно, вы уже знаете что хлопаете, так что нечего обнаруживать.
Мэтт

Да, это верно для некоторых других подходов, и многие из них имеют схожие комментарии. Я просто разъяснял, так как это был недавний ответ с определенным опровержением, и я развел надежды, когда прочитал его. Для записи, однако, вопрос заключается в том, как обнаружить нажатие кнопки назад. Разумный аргумент состоит в том, чтобы сказать, что код, который также будет выполняться в ситуациях, когда кнопка возврата не нажата, без указания того, была ли нажата кнопка возврата, не полностью решает реальный вопрос, даже если, возможно, вопрос мог быть более явно по этому вопросу.
biomiker

1
К сожалению, это возвращает trueк интерактивному жесту смахивания пальцем - с левого края контроллера представления - даже если смахивание не выскочило полностью. Таким образом, вместо того, чтобы проверить это willDisappear, делать это в didDisappearработе.
бадханганеш

1
@badhanganesh Спасибо, отредактированный ответ, чтобы включить эту информацию.
матовый

9

Я играю (или борюсь) с этой проблемой в течение двух дней. IMO лучший подход - просто создать класс расширения и протокол, например так:

@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

Это работает, потому что UINavigationControllerбудет получать вызов navigationBar:shouldPopItem:каждый раз, когда контроллер представления подключен. Там мы обнаруживаем, была ли нажата спина или нет (любая другая кнопка). Единственное, что вам нужно сделать, это реализовать протокол в контроллере вида, где нажата кнопка назад.

Не забудьте вручную вставить контроллер представления внутрь backButtonPressedSel, если все в порядке.

Если у вас уже есть подклассы UINavigationViewControllerи реализованы navigationBar:shouldPopItem:, не волнуйтесь, это не помешает.

Вы также можете быть заинтересованы в отключении жеста спины.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

1
Этот ответ был почти полным для меня, за исключением того, что я обнаружил, что 2 контроллера вида часто появляются. Возвращение YES заставляет вызывающий метод вызывать pop, поэтому вызов pop также означал, что 2 viewcontroller будут вытолкнуты. См. Этот ответ на другой вопрос для большего количества deets (очень хороший ответ, который заслуживает большего количества голосов): stackoverflow.com/a/26084150/978083
Джейсон Ридж

Хороший вопрос, мое описание не было ясно об этом факте. «Не забудьте вручную вывести контроллер представления, если все в порядке», это только для случая возврата «НЕТ», в противном случае поток - это обычное всплывающее окно.
7ynk3r

1
Для ветки "else" лучше вызывать супер-реализацию, если вы не хотите обрабатывать pop самостоятельно и позволять ему возвращать то, что он считает правильным, что в основном является YES, но тогда он также заботится о самом pop и правильно анимирует chevron. .
Бен Sinclair

9

Это работает для меня в iOS 9.3.x с Swift:

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

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

В отличие от других решений здесь, это, кажется, не срабатывает неожиданно.


лучше вместо этого будет использовать willMove
Евгений Гордин

4

Для протокола, я думаю, что это больше того, что он искал ...

    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];
    }

1
Спасибо Пол, это решение довольно простое. К сожалению, значок отличается. Это значок «перемотка назад», а не значок «назад». Может быть, есть способ использовать значок назад ...
Ферран Майлинч

2

Как purrrminatorговорится, ответ elitalonне является полностью правильным, поскольку your stuffбудет выполняться даже при программном вызове контроллера.

Решение, которое я нашел до сих пор, не очень хорошее, но оно работает для меня. Помимо elitalonсказанного, я также проверяю, высовываю ли я программно или нет:

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

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

Вы должны добавить это свойство к своему контроллеру и установить его в YES перед программным выводом:

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

Спасибо за вашу помощь!


2

Лучший способ - использовать методы делегата UINavigationController.

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

Используя это, вы можете узнать, какой контроллер показывает UINavigationController.

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}

Это должно быть помечено как правильный ответ! Можно также добавить еще одну строку, чтобы напомнить людям -> self.navigationController.delegate = self;
Майк Кричли

2

Я решил эту проблему, добавив UIControl к панели навигации с левой стороны.

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];

И вам нужно помнить, чтобы удалить его, когда вид исчезнет:

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

Вот и все!


2

Вы можете использовать обратный вызов кнопки «Назад», например:

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

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

для быстрой версии вы можете сделать что-то вроде в глобальной области видимости

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

Ниже того, что вы поместили в viewcontroller, где вы хотите контролировать действие кнопки назад:

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

    return true
}

1
Не знаю, почему кто-то проголосовал. Это, кажется, лучший ответ.
Авинаш

@ Avinash Откуда navigationShouldPopOnBackButton? Он не является частью публичного API.
Элиталон

@elitalon Извините, это был наполовину ответ. Я думал, что остающийся контекст был там под вопросом. Во всяком случае обновили ответ сейчас
Авинаш

1

Как сказал Coli88, вы должны проверить протокол UINavigationBarDelegate.

В более общем смысле вы также можете использовать пользовательскую - (void)viewWillDisapear:(BOOL)animatedдля выполнения пользовательской работы, когда представление, сохраняемое текущим видимым контроллером представления, собирается исчезнуть. К сожалению, это покрыло бы беспокоящие толчки и случаи популярности.


1

Для Swift с UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}

1

Ответ 7ynk3r был очень близок к тому, что я использовал в конце, но для этого потребовались некоторые изменения:

- (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;
    }
}


0

self.navigationController.isMovingFromParentViewController больше не работает на iOS8 и 9, которые я использую:

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}

-1

(СВИФТ)

окончательно найденное решение .. метод, который мы искали, будет "willShowViewController", который является методом делегата 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
        }
    }
}

1
Проблема с этим подходом в том, что он связан MyViewControllerс PushedController.
Clozach
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.