Визначення при натисканні кнопки «назад» на панелі навігації


135

Мені потрібно виконати деякі дії, коли кнопка "Назад" (повернення до попереднього екрана, повернення до виду батьків) натискається на Navbar.

Чи є якийсь метод, який я можу застосувати, щоб зловити подію та зняти деякі дії, щоб призупинити та зберегти дані до того, як екран зникне?




Я зробив це таким чином, показати рішення тут
Тарас

Відповіді:


316

ОНОВЛЕННЯ: Згідно з деякими коментарями, рішення в оригінальній відповіді, здається, не працює в певних сценаріях в iOS 8+. Я не можу перевірити, що це насправді так, без додаткових деталей.

Для тих із вас, проте в цій ситуації є альтернатива. Визначити, коли з'являється контролер перегляду, можливо, переосмисливши його willMove(toParentViewController:). Основна ідея полягає в тому, що контролер перегляду вискакує, коли parentє nil.

Перевірте «Реалізація контролера Контейнер View» для отримання більш докладної інформації.


З 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

Визначення кнопки "назад" краще робити, коли вимкнений ПК від його батьківського (NavigationController) за допомогою willMoveToParentViewController(_:)АБОdidMoveToParentViewController()

Якщо батьківське значення дорівнює нулю, контролер перегляду вискакує з навігаційного стеку та звільняється. Якщо батько не дорівнює нулю, він додається до стеку та подається.

// 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 в стилі 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. ДеталіVC: подробиці про річ
  3. НалаштуванняVC: деякі варіанти речі

Дозволяє слідкувати за дзвінками в той час, detailVCколи ви переходите від пункту listVCдоsettingsVC і назад доlistVC

Список> Деталі ( деталізація натиснітьVC) Detail.viewDidAppear<- з'являється
Деталі> Налаштування (натисніть налаштуванняVC) Detail.viewDidDisappear<- зникають

І коли ми повертаємось назад ...
Налаштування> Деталі (параметри випускуVC) <- Detail.viewDidAppearз'являються
деталі> Список (деталі випускуVC) Detail.viewDidDisappear<- зникають

Зауважте, що viewDidDisappearвикликається кілька разів, не тільки при поверненні назад, але і при русі вперед. Для швидкої операції, яка може бути бажаною, але для більш складної операції, як мережевий дзвінок, щоб зберегти, це може не зробити.


Просто примітка, користувач didMoveToParantViewController:повинен працювати, коли подання більше не видно. Корисно для iOS7 з interactiveGesutre
WCByrne

didMoveToParentViewController * є друкарська помилка
thewormsterror

Не забудьте зателефонувати [super willMoveToParentViewController: parent]!
ScottyB

2
Батьківський параметр дорівнює нулю, коли ви переходите на батьківський контролер подання, і не-нульовий, коли відображається подання, в якому відображається цей метод. Ви можете скористатися цим фактом, щоб зробити дію лише тоді, коли натиснута кнопка Назад, а не при надходженні на перегляд. Зрештою, це було оригінальне питання. :)
Майк

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
Другий метод був єдиним, який працював на мене. Перший метод був також закликаний до мого представлення, що не було прийнятним для мого випадку використання.
Маркшилінг

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 виявиться невдалим. Це не вийшло з ладу; ти його зламав.

ПРИМІТКА . Справжньою проблемою є, як правило, другий контролер перегляду, який виявляє, що перший контролер огляду вискочив. Дивіться також більш загальну дискусію тут: Уніфікований UIViewController "став передовим" виявленням?

EDIT Коментар говорить про те, що це має бути, viewDidDisappearа не viewWillDisappear.


Цей код виконується при натисканні кнопки "Назад", але також виконується, якщо програмне забезпечення вимкнено програмно.
біомікер

@biomiker Звичайно, але це було б і в інших підходах. Вискочити - вискочити. Питання полягає в тому, як виявити поп, коли ви не програвали програмно. Якщо ви програмуєте програмно, ви вже знаєте , що вискакує, тому нічого не можна виявити.
мат

Так, це стосується кількох інших підходів, і багато хто з них мають подібні коментарі. Я тільки уточнював, оскільки це була нещодавня відповідь із конкретним спростуванням, і я сподівався, коли я прочитав її. Для запису, проте, питання полягає в тому, як виявити натиснення кнопки "назад". Це розумний аргумент, щоб сказати, що код, який також буде виконуватися в ситуаціях, коли кнопку назад не натискають, не вказуючи, чи була натиснута кнопка "назад", не повністю вирішує справжнє питання, навіть якщо, можливо, питання могло бути більше явна з цього приводу.
біомікер

1
На жаль, це повертається trueза інтерактивним жестом попливу пальцем - з лівого краю контролера перегляду, - навіть якщо пальцем не вдалося повністю пропустити його. Тож замість того, щоб перевіряти це willDisappear, робите це у didDisappearтворах.
badhanganesh

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

9

Я граю (або борюся) з цією проблемою два дні. ІМО найкращим підходом є просто створення класу розширень та протоколу, наприклад:

@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 контролера перегляду часто будуть вискакуватися. Повернення ДА викликає метод виклику викликати поп, тому виклик попсу також означав, що будуть вискакувати 2 контролера перегляду. Дивіться цей відповідь на інше питання для більш deets (дуже хороший відповідь , який заслуговує більш upvotes): stackoverflow.com/a/26084150/978083
Jason Ridge

Хороший момент, моє опис не було зрозумілим щодо цього факту. "Не забудьте вручну проскакувати контролер перегляду, якщо все в порядку", це лише у випадку повернення "НІ", інакше потік - це звичайний екран.
7ynk3r

1
Для гілки "else" краще викликати суперреалізацію, якщо ви не хочете самостійно обробляти поп і нехай він повертає все, що вважає за правильне, що здебільшого ТАК, але він також піклується про поп, а потім анімує chevron належним чином .
Бен Сінклер

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

Ви повинні додати це властивість до свого контролера та встановити його ТАК перед тим, як програмно вискакувати:

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

Знизу ви вводите контролер перегляду, де ви хочете керувати дією кнопки назад:

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

    return true
}

1
Не знаю, чому хтось проголосував. Це, здається, є найкращою відповіддю.
Авінаш

@Avinash Звідки navigationShouldPopOnBackButtonберуться? Він не є частиною загальнодоступного API.
еліталон

@elitalon Вибачте, це була половина відповіді. Я думав, що залишився контекст тут під питанням. Так чи інакше, зараз оновили відповідь
Avinash

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

(SWIFT)

нарешті знайдено рішення. Метод, який ми шукали, - це "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.