iOS导航控制器中的后退按钮回调

Posted

技术标签:

【中文标题】iOS导航控制器中的后退按钮回调【英文标题】:back button callback in navigationController in iOS 【发布时间】:2011-07-10 05:36:07 【问题描述】:

我已将视图推送到导航控制器上,当我按下后退按钮时,它会自动转到上一个视图。在将视图从堆栈中弹出之前,我想在按下后退按钮时做一些事情。哪个是返回键回调函数?

【问题讨论】:

Setting action for back button in navigation controller 的可能重复项 查看这个[解决方案][1],它也保留了后退按钮样式。 [1]:***.com/a/29943156/3839641 【参考方案1】:

也许有点太晚了,但我以前也想要同样的行为。我采用的解决方案在 App Store 目前的一款应用中运行良好。由于我还没有看到有人使用类似的方法,所以我想在这里分享一下。这个解决方案的缺点是它需要子类化UINavigationController。虽然使用Method Swizzling 可能有助于避免这种情况,但我并没有走那么远。

所以,默认的后退按钮实际上是由UINavigationBar 管理的。当用户点击返回按钮时,UINavigationBar 会通过调用 navigationBar(_:shouldPop:) 询问其代表是否应该弹出顶部的 UINavigationItemUINavigationController 实际上实现了这一点,但它没有公开声明它采用UINavigationBarDelegate(为什么!?)。要拦截此事件,请创建UINavigationController 的子类,声明其符合UINavigationBarDelegate 并实现navigationBar(_:shouldPop:)。如果应该弹出顶部项目,则返回 true。如果应该保留,请返回 false

有两个问题。首先是您必须在某个时候调用navigationBar(_:shouldPop:)UINavigationController 版本。但是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
    

你可以看看我的演示here。

【讨论】:

这是一个很棒的解决方案,应该写进一篇博文!对于我现在正在搜索的内容似乎有点过头了,但在其他情况下,这肯定值得一试。【参考方案2】:

这就是我在 Swift 中的作用:

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

    super.viewWillDisappear(animated)

【讨论】:

【参考方案3】:

我最终得到了这个解决方案。当我们点击返回按钮时调用 viewDidDisappear 方法。我们可以通过调用返回 true 的 isMovingFromParentViewController 选择器来检查。我们可以传回数据(使用委托)。希望这对某人有所帮助。

-(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]【参考方案4】:

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

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

    

在推送视图时也会调用此方法。所以检查 parent==nil 是为了从堆栈中弹出视图控制器

【讨论】:

【参考方案5】:

这是我实现的另一种方式(没有用展开 segue 对其进行测试,但它可能不会区分,正如其他人在此页面上的其他解决方案中所说的那样)让父视图控制器在子视图控制器之前执行操作它推送的 VC 从视图堆栈中弹出(我在原始 UINavigationController 的基础上使用了几个级别)。这也可以用于在 childVC 被推送之前执行操作。这具有使用 ios 系统后退按钮的额外优势,而不必创建自定义 UIBarButtonItem 或 UIButton。

    让你的父 VC 采用UINavigationControllerDelegate 协议并注册委托消息:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad 
        self.navigationcontroller.delegate = self;
    
    

    MyParentViewController中实现这个UINavigationControllerDelegate实例方法:

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

    如果在上述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;
    

【讨论】:

【参考方案6】:

如果您正在使用 Storyboard 并且来自 push segue,您也可以直接覆盖 shouldPerformSegueWithIdentifier:sender:

【讨论】:

【参考方案7】:

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

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

【讨论】:

【参考方案8】:

有一个比询问 viewControllers 更合适的方法。您可以使您的控制器成为具有后退按钮的导航栏的代表。这是一个例子。在您要处理按下后退按钮的控制器的实现中,告诉它它将实现 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;

【讨论】:

对我不起作用..可怜,因为它很瘦。 "*** 由于未捕获的异常 'NSInternalInconsistencyException' 导致应用程序终止,原因:'无法在控制器管理的 UINavigationBar 上手动设置委托。'" 不幸的是,这不适用于 UINavigationController,相反,您需要一个带有 UINavigationBar 的标准 UIViewController。这确实意味着您不能利用 NavigationController 为您提供的几个自动视图控制器推送和弹出。对不起! 我只是使用了 UINavigationBar 而不是 NavigationBarController,然后它就可以正常工作了。我知道问题是关于 NavigationBarController,但这个解决方案很精简。【参考方案9】:

如果您不能使用“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,因此,在您的常规 viewController 的 viewDidLoad 方法中执行以下操作:

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

【讨论】:

【参考方案10】:

在我看来是最好的解决方案。

- (void)didMoveToParentViewController:(UIViewController *)parent

    if (![parent isEqual:self.parentViewController]) 
         NSLog(@"Back pressed");
    

但它只适用于 iOS5+

【讨论】:

这种技术无法区分后退按钮点击和展开转场。 willMoveToParentViewController和viewWillDisappear方法没有说明必须销毁控制器,didMoveToParentViewController是对的【参考方案11】:

最好覆盖后退按钮,这样您就可以处理事件弹出视图以进行用户确认等操作。

在 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 没有在点击后退按钮时调用的方法。 此技术允许验证视图控制器的数据并从导航控制器的后退按钮有条件地返回。 这个方案打破了iOS 7+的边缘滑动功能【参考方案12】:

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

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

【讨论】:

此代码不仅在用户点击返回按钮时执行,而且在每次弹出视图时都会执行(例如,当右侧有完成或保存按钮时)。 或者在前进到新视图时。 当用户从左边缘平移时也会调用此方法(interactivePopGestureRecognizer)。就我而言,我专门寻找用户何时按下而不是从左边缘平移。 并不意味着后退按钮是原因。例如,可能是一个放松的转场。 我有一个疑问,为什么我们不应该在 viewDidDisappear 中这样做?

以上是关于iOS导航控制器中的后退按钮回调的主要内容,如果未能解决你的问题,请参考以下文章

IOS应用导航栏后退按钮

以编程方式调用 iOS 上的导航控制器后退按钮

Ios 导航 - 自定义后退按钮或从堆栈中删除视图控制器?

iOS13导航栏后退栏按钮项目色调颜色

导航控制器中的后退按钮动画

导航栏控制器中的自定义后退按钮