обратный вызов кнопки возврата в navigationController в iOS


102

Я поместил представление на контроллер навигации, и когда я нажимаю кнопку «Назад», он автоматически переходит к предыдущему представлению. Я хочу сделать несколько вещей при нажатии кнопки «Назад» перед тем, как вывести представление из стека. Что такое функция обратного вызова кнопки "Назад"?



Ознакомьтесь с этим [решением] [1], в котором также сохраняется стиль кнопки «Назад». [1]: stackoverflow.com/a/29943156/3839641
Сарасранглт,

Ответы:


162

Уильям Джокуши в ответ решить эту проблему с помощью простого трюка.

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

32
Этот код выполняется не только, когда пользователь нажимает кнопку «Назад», но и в каждом случае всплывающего окна (например, при наличии кнопки «Готово» или «Сохранить» справа).
значение-имеет значение

7
Или при переходе к новому просмотру.
GuybrushThreepwood

Это также вызывается, когда пользователь перемещается от левого края (InteractivePopGestureRecognizer). В моем случае я специально ищу, когда пользователь нажимает назад, НЕ панорамируя от левого края.
Кайл Клегг

2
Это не значит, что причиной была кнопка «Назад». Это может быть, например, расслабляющий переход.
smileBot

1
Я сомневаюсь, почему бы нам не сделать это в viewDidDisappear?
JohnVanDijk

85

На мой взгляд лучшее решение.

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

Но работает только с iOS5 +


3
Этот метод не позволяет отличить нажатие кнопки «Назад» от перехода при размотке.
smileBot

Методы willMoveToParentViewController и viewWillDisappear не объясняют, что контроллер должен быть уничтожен, didMoveToParentViewController прав
Хэнк,

27

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

в viewDidLoad создайте UIBarButtonItem и установите для него self.navigationItem.leftBarButtonItem, передавая sel

- (void) viewDidLoad
{
// change the back button to cancel and add an event handler
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@”back
style:UIBarButtonItemStyleBordered
target:self
action:@selector(handleBack:)];

self.navigationItem.leftBarButtonItem = backButton;
[backButton release];

}
- (void) handleBack:(id)sender
{
// pop to root view controller
[self.navigationController popToRootViewControllerAnimated:YES];

}

Затем вы можете сделать такие вещи, как поднять UIAlertView для подтверждения действия, затем открыть контроллер представления и т. Д.

Или вместо создания новой кнопки возврата вы можете соответствовать методам делегата UINavigationController для выполнения действий при нажатии кнопки возврата.


У UINavigationControllerDelegateнего нет методов, которые вызываются при нажатии кнопки возврата.
значение-имеет значение

Этот метод позволяет проверить данные контроллера представления и условный возврат от кнопки возврата контроллера навигации.
gjpc

Это решение ломает функцию смахивания края в iOS 7+
Лирон Яхдав

9

Это правильный способ обнаружить это.

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        //do stuff

    }
}

этот метод вызывается также при отправке представления. Таким образом, проверка parent == nil предназначена для извлечения контроллера представления из стека


9

Я получаю эти решения. Когда мы нажимаем кнопку возврата, вызывается метод viewDidDisappear. мы можем проверить, вызвав селектор isMovingFromParentViewController, который возвращает true. мы можем передать данные обратно (используя делегата). надеюсь, что это кому-то поможет.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

    }
    if (self.isMovingFromParentViewController) {
       //moving back
        //pass to viewCollection delegate and update UI
        [self.delegateObject passBackSavedData:self.dataModel];

    }
}

Не забывайте[super viewDidDisappear:animated]
SamB

9

Может быть, уже слишком поздно, но я и раньше хотел такого же поведения. И решение, которое я выбрал, довольно хорошо работает в одном из приложений, которые сейчас есть в App Store. Поскольку я не видел, чтобы кто-то использовал подобный метод, я хотел бы поделиться им здесь. Обратной стороной этого решения является необходимость создания подклассов UINavigationController. Хотя с помощью метода Swizzling могло бы помочь избежать этого, я не пошел так далеко.

Итак, кнопка возврата по умолчанию фактически управляется UINavigationBar. Когда пользователь нажимает кнопку «Назад», UINavigationBarспросите своего делегата, следует ли открывать верхнюю часть UINavigationItem, позвонив navigationBar(_:shouldPop:). UINavigationControllerна самом деле реализует это, но публично не заявляет, что принимает UINavigationBarDelegate(почему !?). Чтобы перехватить это событие, создайте подкласс UINavigationController, объявите его соответствие UINavigationBarDelegateи реализуйте navigationBar(_:shouldPop:). Вернитесь, trueесли должен выскочить верхний предмет. Вернись, falseесли останется.

Есть две проблемы. Во-первых, вы должны вызвать UINavigationControllerверсиюnavigationBar(_:shouldPop:) в какой-то момент . Но UINavigationBarControllerпублично не заявляет о соответствииUINavigationBarDelegate , попытка вызвать это приведет к ошибке времени компиляции. Решение, которое я выбрал, - использовать среду выполнения Objective-C, чтобы напрямую получить реализацию и вызвать ее. Пожалуйста, дайте мне знать, есть ли у кого-нибудь лучшее решение.

Другая проблема заключается в том, что navigationBar(_:shouldPop:)сначала вызывается, popViewController(animated:)если пользователь нажимает кнопку «Назад». Порядок меняется на противоположный, если контроллер представления выталкивается вызовом popViewController(animated:). В этом случае я использую логическое значение, чтобы определить,popViewController(animated:) вызывается раньше, navigationBar(_:shouldPop:)что означает, что пользователь нажал кнопку возврата.

Кроме того, я делаю расширение, UIViewControllerчтобы позволить контроллеру навигации запрашивать контроллер представления, следует ли его выскакивать, если пользователь нажимает кнопку «Назад». Контроллеры просмотра могут вернуться falseи выполнить любые необходимые действия, а также позвонить popViewController(animated:)позже.

class InterceptableNavigationController: UINavigationController, UINavigationBarDelegate {
    // If a view controller is popped by tapping on the back button, `navigationBar(_:, shouldPop:)` is called first follows by `popViewController(animated:)`.
    // If it is popped by calling to `popViewController(animated:)`, the order reverses and we need this flag to check that.
    private var didCallPopViewController = false

    override func popViewController(animated: Bool) -> UIViewController? {
        didCallPopViewController = true
        return super.popViewController(animated: animated)
    }

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // If this is a subsequence call after `popViewController(animated:)`, we should just pop the view controller right away.
        if didCallPopViewController {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        }

        // The following code is called only when the user taps on the back button.

        guard let vc = topViewController, item == vc.navigationItem else {
            return false
        }

        if vc.shouldBePopped(self) {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        } else {
            return false
        }
    }

    func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
        didCallPopViewController = false
    }

    /// Since `UINavigationController` doesn't publicly declare its conformance to `UINavigationBarDelegate`,
    /// trying to called `navigationBar(_:shouldPop:)` will result in a compile error.
    /// So, we'll have to use Objective-C runtime to directly get super's implementation of `navigationBar(_:shouldPop:)` and call it.
    private func originalImplementationOfNavigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        let sel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPop:))
        let imp = class_getMethodImplementation(class_getSuperclass(InterceptableNavigationController.self), sel)
        typealias ShouldPopFunction = @convention(c) (AnyObject, Selector, UINavigationBar, UINavigationItem) -> Bool
        let shouldPop = unsafeBitCast(imp, to: ShouldPopFunction.self)
        return shouldPop(self, sel, navigationBar, item)
    }
}

extension UIViewController {
    @objc func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        return true
    }
}

А в вашем представлении контроллеры реализовывать shouldBePopped(_:). Если вы не реализуете этот метод, по умолчанию контроллер представления будет появляться, как только пользователь нажмет кнопку «Назад», как обычно.

class MyViewController: UIViewController {
    override func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        let alert = UIAlertController(title: "Do you want to go back?",
                                      message: "Do you really want to go back? Tap on \"Yes\" to go back. Tap on \"No\" to stay on this screen.",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in
            navigationController.popViewController(animated: true)
        }))
        present(alert, animated: true, completion: nil)
        return false
    }
}

Вы можете посмотреть мою демонстрацию здесь .

введите описание изображения здесь


Это отличное решение, и его стоит добавить в блог! Кажется, это излишне для того, что я ищу прямо сейчас, но в других обстоятельствах это определенно стоит попробовать.
ASSeeger

6

Для "ПЕРЕД извлечением представления из стека":

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        NSLog(@"do whatever you want here");
    }
}

5

Есть более подходящий способ, чем спросить у viewControllers. Вы можете сделать свой контроллер делегатом навигационной панели, у которой есть кнопка возврата. Вот вам пример. В реализации контроллера, в котором вы хотите обрабатывать нажатие кнопки возврата, сообщите ему, что он будет реализовывать протокол UINavigationBarDelegate:

@interface MyViewController () <UINavigationBarDelegate>

Затем где-нибудь в вашем коде инициализации (возможно, в viewDidLoad) сделайте ваш контроллер делегатом его панели навигации:

self.navigationController.navigationBar.delegate = self;

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

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}

4
у меня не сработало .. жалко, потому что тощий. «*** Завершение работы приложения из-за неперехваченного исключения« NSInternalInconsistencyException », причина:« Невозможно вручную установить делегат на UINavigationBar, управляемый контроллером ».»
DynamicDan

К сожалению, это не будет работать с UINavigationController, вместо этого вам понадобится стандартный UIViewController с UINavigationBar в нем. Это означает, что вы не можете воспользоваться преимуществами нескольких автоматических нажатий и выталкиваний контроллеров представления, которые предоставляет вам NavigationController. Сожалею!
Карлос Гусман

Я просто использовал UINavigationBar вместо NavigationBarController, и тогда он отлично работает. Я знаю, что вопрос касается NavigationBarController, но это решение скудное.
appsun создан

3

Если вы не можете использовать «viewWillDisappear» или аналогичный метод, попробуйте создать подкласс UINavigationController. Это класс заголовка:

#import <Foundation/Foundation.h>
@class MyViewController;

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Класс реализации:

#import "CCNavigationController.h"
#import "MyViewController.h"

@implementation CCNavigationController {

}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    @"This is the moment for you to do whatever you want"
    [self.viewController doCustomMethod];
    return [super popViewControllerAnimated:animated];
}

@end

С другой стороны, вам нужно связать этот viewController с вашим настраиваемым NavigationController, поэтому в вашем методе viewDidLoad для вашего обычного viewController сделайте следующее:

@implementation MyViewController {
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        ((CCNavigationController*)self.navigationController).viewController = self;
    }
}

3

Вот еще один способ, который я реализовал (не тестировал его с помощью разматывающего перехода, но он, вероятно, не будет отличаться, как другие заявляли в отношении других решений на этой странице), чтобы родительский контроллер представления выполнял действия до того, как дочерний VC он нажал выскакивает из стека представления (я использовал его на пару уровней ниже исходного UINavigationController). Это также можно использовать для выполнения действий до того, как childVC будет вытолкнут. Это дает дополнительное преимущество работы с кнопкой возврата системы iOS вместо необходимости создавать пользовательский UIBarButtonItem или UIButton.

  1. Попросите родительский VC принять UINavigationControllerDelegateпротокол и зарегистрироваться для сообщений делегата:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Реализуйте этот UINavigationControllerDelegateметод экземпляра в MyParentViewController:

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
        // Test if operation is a pop; can also test for a push (i.e., do something before the ChildVC is pushed
        if (operation == UINavigationControllerOperationPop) {
            // Make sure it's the child class you're looking for
            if ([fromVC isKindOfClass:[ChildViewController class]]) {
                // Can handle logic here or send to another method; can also access all properties of child VC at this time
                return [self didPressBackButtonOnChildViewControllerVC:fromVC];
            }
        }
        // If you don't want to specify a nav controller transition
        return nil;
    }
  3. Если вы укажете конкретную функцию обратного вызова в приведенном выше UINavigationControllerDelegateметоде экземпляра

    -(id <UIViewControllerAnimatedTransitioning>)didPressBackButtonOnAddSearchRegionsVC:(UIViewController *)fromVC {
        ChildViewController *childVC = ChildViewController.new;
        childVC = (ChildViewController *)fromVC;
    
        // childVC.propertiesIWantToAccess go here
    
        // If you don't want to specify a nav controller transition
        return nil;

    }


1

Вот что у меня работает в Swift:

override func viewWillDisappear(_ animated: Bool) {
    if self.navigationController?.viewControllers.index(of: self) == nil {
        // back button pressed or back gesture performed
    }

    super.viewWillDisappear(animated)
}

0

Если вы используете раскадровку и выходите из push-перехода, вы также можете просто переопределить shouldPerformSegueWithIdentifier:sender:.

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