检测导航栏上的“返回”按钮何时被按下

146

当导航栏的返回按钮(返回上一屏幕,返回父视图)被按下时,我需要执行一些操作。

有没有一些方法可以实现捕获事件并触发一些动作,在屏幕消失之前暂停并保存数据?


可能是在导航控制器中设置返回按钮操作的重复问题。 - nielsbot
1
请查看此线程中的解决方案 - Jiri Volejnik
我是这样做的在此展示决策 - Taras
18个回答

323

更新: 根据一些评论,原回答中的解决方案似乎在iOS 8+的某些情况下无法正常工作。我没有更多细节无法验证这是否属实。

然而,对于那些处于这种情况的人,有一个替代方案。可以通过重写 willMove(toParentViewController:) 来检测视图控制器何时被弹出。基本思路是当 parentnil 时,视图控制器正在被弹出。

请参阅 “实现容器视图控制器” 了解更多详情。


自从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。与此不同,我更喜欢我的方法,因为它将执行所需的任务与触发事件(即按下返回按钮)分离。


@Sam 确保你在正确的方法中调用 self.isMovingFromParentViewController。如文档所述,"只有在以下方法内调用此方法时,该方法才返回 YES:" viewWillDisappear:viewDidDisappear:。同时,请确保你正在使用导航控制器,而不是呈现模态视图控制器。 - elitalon
4
当我使用popToRootViewControllerAnimated以编程方式弹出导航堆栈时,self.isMovingFromParentViewController的值为TRUE - 而没有触摸返回按钮。这种情况下应该否决你的答案?(主题说“在导航栏上按下'返回'按钮”) - kas-kad
2
非常好的答案,非常感谢。在Swift中,我使用了以下代码:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } } - Camillo
2
你应该只在 -viewDidDisappear: 方法中执行此操作,因为有可能会出现 -viewWillDisappear: 没有 -viewDidDisappear: 的情况(例如当你开始滑动以取消导航控制器项时,然后取消该滑动)。 - Heath Borders
3
看起来这不再是一个可靠的解决方案了。我第一次使用它时效果很好(当时是iOS 10)。但现在我偶然发现它已经停止工作了(iOS 11)。不得不转而使用“willMove(toParentViewController)”的解决方案。 - Vitalii
显示剩余14条评论

105

当点击返回按钮时,viewWillAppear()viewDidDisappear()确实会被调用,但它们也会在其他时间被调用。请参见答案末尾的更多内容。

使用UIViewController.parent

检测返回按钮最好在视图控制器从其父视图控制器(导航控制器)中移除时进行,可以借助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
    }
}

使用 didMove 代替 willMove,并检查 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
        }
    }
}
  1. ListVC: 一个包含各种事物的表格视图
  2. DetailVC: 有关某一事物的详细信息
  3. SettingsVC: 某一事物的一些选项

当从listVCsettingsVC并返回listVC时,请遵循对detailVC的调用。

List > Detail(推送 detailVC)Detail.viewDidAppear <- appear
Detail > Settings(推送 settingsVC)Detail.viewDidDisappear <- disappear

返回时...
Settings > Detail(弹出 settingsVC)Detail.viewDidAppear <- appear
Detail > List(弹出 detailVC)Detail.viewDidDisappear <- disappear

请注意,viewDidDisappear不仅在后退时被调用,而且在前进时也会被多次调用。对于快速操作可能是需要的,但对于像网络调用保存这样的更复杂的操作可能不是需要的。


只是一个提示,用户可以使用didMoveToParantViewController:方法在视图不再可见时执行操作。这对于具有interactiveGesture的iOS7非常有帮助。 - WCByrne
didMoveToParentViewController* 有一个打字错误。 - thewormsterror
不要忘记调用 [super willMoveToParentViewController:parent]! - ScottyB
2
当您弹出到父视图控制器时,parent参数为nil;当显示此方法的视图时,parent参数为非nil。您可以利用这个事实,在仅在按下“返回”按钮时执行操作,而不是在到达视图时执行操作。毕竟,这就是最初的问题。 :) - Mike
1
当使用 _ = self.navigationController?.popViewController(animated: true) 进行编程时,也会调用此方法,因此它不仅在按下返回按钮时被调用。我正在寻找一个仅在按下返回按钮时起作用的调用。 - Ethan Allen
@EthanAllen 我能想到的唯一方法是在“停止”选项下进行解除。 - WCByrne

20

声称这不起作用的人是错误的:

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 "became frontmost" detection?

编辑 有评论建议应该使用viewDidDisappear而不是viewWillDisappear


3
当用户点击返回按钮时,此代码将被执行;但是如果视图控制器被程序化地弹出,它也会被执行。 - biomiker
2
@biomiker 当然,其他方法也是如此。弹出就是弹出。问题在于如何检测在程序中没有弹出的情况下进行弹出。如果您在程序中进行弹出,那么您已经知道自己正在弹出,因此无需检测。 - matt
1
是的,这也适用于其他几种方法,其中许多都有类似的评论。我只是澄清一下,因为这是最近的一个回答,并且有一个具体的反驳,当我读到它时,我抱有希望。但是,值得记录的是,问题是如何检测后退按钮的按下。有理由认为,在不指示是否按下后退按钮的情况下,在其他情况下执行的代码并不能完全解决实际问题,即使问题在这一点上可能可以更明确。 - biomiker
6
不幸的是,即使滑动没有完全弹出,这也会对交互式滑动弹出手势(从视图控制器的左边缘)返回“true”。因此,不要在willDisappear中检查它,在didDisappear中检查会更好。 - badhanganesh
2
@badhanganesh 谢谢,已编辑答案并包含该信息。 - matt
显示剩余3条评论

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

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个视图控制器。返回YES会导致调用方法调用pop,因此也调用pop意味着将弹出2个视图控制器。请参阅另一个问题上的此答案以获取更多详细信息(一个非常好的答案,值得更多的赞):https://dev59.com/QnnZa4cB1Zd3GeqPuccv#26084150 - Jason Ridge
好的,我的描述没有清楚表明这一事实。"如果一切正常,请记得手动弹出视图控制器"只适用于返回"NO"的情况,否则流程是正常的弹出。 - 7ynk3r
1
对于“else”分支,如果您不想自己处理pop并让它返回任何它认为是正确的内容(通常是YES),最好调用super实现,但它也会自己处理pop并适当地动画化chevron。 - pronebird

8

这对我在iOS 9.3.x和Swift中有效:

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

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

与其他解决方案不同,这个似乎不会意外触发。

最好使用willMove而不是。 - Eugene Gordin
不确定willMove是否存在与willDisappear相同的问题:用户可以通过滑动开始解除视图控制器,willDisappear将被调用,但用户仍然可以取消滑动! - Mycroft Canner

7
您可以使用后退按钮回调函数,像这样:

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

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

对于 Swift 版本,您可以在全局范围内执行以下操作:
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
}

2
不知道为什么有人踩了这个回答。这似乎是迄今为止最好的答案。 - Avinash
@Avinash,“navigationShouldPopOnBackButton”是从哪里来的?它不是公共API的一部分。 - elitalon
@elitalon 抱歉,这只是一半的答案。我以为问题中还有剩余的上下文。无论如何,我现在已经更新了答案。 - Avinash
我同意。这是一个被低估的解决方案,它使用了系统返回按钮和"<"以及后退菜单。我总是更喜欢在可能的情况下将我的代码馈入系统回调中,而不是模仿UI元素。 - RyuX51

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
谢谢Paul,这个解决方案非常简单。不幸的是,图标不同。这是“倒带”图标,而不是返回图标。也许有一种方法可以使用返回图标... - Ferran Maylinch

3
最好的方法是使用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; - Mike Critchley

2

对于带有UINavigationController的Swift:

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

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