iOS中navigationController的返回按钮回调函数

106

我已经将一个视图推入导航控制器,当我按下返回按钮时,它会自动回到上一个视图。在弹出视图之前,我想在返回按钮被按下时执行一些操作。哪个是返回按钮的回调函数?


可能是在导航控制器中设置返回按钮操作的重复问题。 - Zakaria
请查看此解决方案,它还保留了返回按钮的样式。 - Sarasranglt
12个回答

163

William Jockusch的答案用一个简单的技巧解决了这个问题。

-(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
这段代码不仅在用户点击返回按钮时执行,而且在视图弹出的每个事件中执行(例如,在右侧有完成或保存按钮时)。 - meaning-matters
7
当转向新视角时。 - GuybrushThreepwood
这也被称为当用户从左边缘滑动时调用(interactivePopGestureRecognizer)。在我的情况下,我特别寻找的是当用户按返回按钮而不是从左边缘滑动时的情况。 - Kyle Clegg
2
并不意味着返回按钮是原因。例如,可能是一个取消跳转操作。 - SmileBot
你为什么在 if 之后调用 super 函数?据我所知,它应该作为第一行被调用。请参考这篇帖子的答案:https://dev59.com/RXA75IYBdhLWcg3wVniI#3348425 - Yuchen
1
我有一个疑问,为什么我们不应该在viewDidDisappear中这样做呢? - JohnVanDijk

85

我认为这是最好的解决方案。

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

但它只能在iOS5及以上版本中使用。


3
该技术不能区分返回按钮的点击和取消 unwind segue 操作。 - SmileBot
willMoveToParentViewController和viewWillDisappear方法并没有说明控制器必须被销毁,正确的方法是didMoveToParentViewController。 - Hank

27

最好覆盖后退按钮,以便您可以在视图弹出之前处理事件,例如用户确认。

在viewDidLoad中创建一个UIBarButtonItem,并将self.navigationItem.leftBarButtonItem设置为这个UIBarButtonItem,传入一个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 没有在返回按钮被点击时调用的方法。 - meaning-matters
该技术允许验证视图控制器的数据,并有条件地从导航控制器的返回按钮返回。 - gjpc
这个解决方案破坏了iOS 7+的边缘滑动功能。 - Liron Yahdav

12
也许有点晚了,但我之前也想要同样的行为。我采用的解决方案在当前App Store上的某个应用程序中效果很好。由于我没有看过任何人使用类似的方法,所以我想在这里分享一下。这种解决方案的缺点是需要子类化 UINavigationController 。虽然使用Method Swizzling可能有助于避免这种情况,但我没有走得那么远。
因此,默认的返回按钮实际上是由 UINavigationBar 管理的。当用户点击返回按钮时, UINavigationBar 通过调用 navigationBar( _:shouldPop :) 询问其委托是否应弹出顶部 UINavigationItem UINavigationController 实际上实现了这一点,但它没有公开声明它采用了 UINavigationBarDelegate (为什么!?)。为拦截此事件,请创建 UINavigationController 的子类,声明其符合 UINavigationBarDelegate 并实现 navigationBar(_:shouldPop :) 。如果应弹出顶部项,则返回 true 。如果应保留,则返回 false
有两个问题。第一个是你必须在某个时间点调用UINavigationController版本的navigationBar(_:shouldPop:)。但是UINavigationBarController没有公开声明其符合UINavigationBarDelegate,尝试调用它会导致编译时错误。我采用的解决方案是使用Objective-C运行时直接获取实现并调用它。如果有更好的解决方案,请告诉我。
另一个问题是,如果用户点击返回按钮,则先调用navigationBar(_:shouldPop:),然后调用popViewController(animated:)。如果通过调用popViewController(animated:)弹出视图控制器,则顺序相反。在这种情况下,我使用布尔值来检测是否在调用navigationBar(_:shouldPop:)之前调用了popViewController(animated:),这意味着用户已经点击了返回按钮。
此外,我创建了一个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
    }
}

你可以在这里查看我的演示。

enter image description here


这是一个很棒的解决方案,应该写成博客文章!对于我现在正在寻找的东西来说似乎有点过头了,但在其他情况下,这肯定值得一试。 - appfrosch

11

这是检测此问题的正确方法。

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

    }
}

当视图被推入时,会调用这种方法。因此检查parent==nil是为了从堆栈中弹出视图控制器。


9
我最终得出了这个解决方案。当我们点击返回按钮时,viewDidDisappear方法被调用。我们可以通过调用isMovingFromParentViewController选择器来检查其是否返回true。我们可以通过代理将数据传回(使用Delegate)。希望这对某些人有所帮助。
-(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

6

对于“从堆栈中弹出视图之前”:

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

5

有一种比询问viewControllers更合适的方法。您可以将控制器设置为具有返回按钮的navigationBar的委托。以下是一个示例。在希望处理返回按钮按下的控制器的实现中,告诉它将实现UINavigationBarDelegate协议:

@interface MyViewController () <UINavigationBarDelegate>

在您的初始化代码中的某个位置(可能在viewDidLoad中),使您的控制器成为其导航栏的代理:

self.navigationController.navigationBar.delegate = self;

最后,实现shouldPopItem方法。当用户按下返回按钮时,该方法将被调用。如果您在堆栈中有多个控制器或导航项,则可能需要检查正在弹出哪些导航项(使用item参数),以便在您期望时进行自定义操作。以下是一个示例:

-(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
对我没用,很遗憾因为它很简洁。"*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot manually set the delegate on a UINavigationBar managed by a controller.'" - DynamicDan
很遗憾,这种方法不能在UINavigationController中使用,相反,您需要一个带有UINavigationBar的标准UIViewController。这意味着您无法利用NavigationController提供的多个自动视图控制器推送和弹出功能。抱歉! - Carlos Guzman
我刚刚使用了UINavigationBar而不是NavigationBarController,然后它就正常工作了。我知道问题是关于NavigationBarController的,但这个解决方案更加简洁。 - appsunited

3

这是我另一种实现方式(没有测试过取消 segue,但它可能不会有区别,因为其他人已经在本页上述解决方案中提到),以使父视图控制器在推送的子 VC 从视图堆栈中弹出之前执行操作(我在原始 UINavigationController 的几个级别下使用了此方法)。这也可以用于在推送 childVC 之前执行操作。这样做的附加优势是与 iOS 系统返回按钮配合使用,而无需创建自定义 UIBarButtonItem 或 UIButton。

  1. Have your parent VC adopt the UINavigationControllerDelegate protocol and register for delegate messages:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
    
  2. Implement this UINavigationControllerDelegate instance method in 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. If you specify a specific callback function in the above UINavigationControllerDelegate instance method

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

    }


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

另一方面,您需要将此视图控制器链接到自定义导航控制器。因此,在常规视图控制器的viewDidLoad方法中执行以下操作:
@implementation MyViewController {
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        ((CCNavigationController*)self.navigationController).viewController = self;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接