137
votes

I need to perform some actions when the back button(return to previous screen, return to parent-view) button is pressed on a Navbar.

Is there some method I can implement to catch the event and fire off some actions to pause and save data before the screen disappears?

18

18 Answers

321
votes

UPDATE: According to some comments, the solution in the original answer does not seem to work under certain scenarios in iOS 8+. I can't verify that that is actually the case without further details.

For those of you however in that situation there's an alternative. Detecting when a view controller is being popped is possible by overriding willMove(toParentViewController:). The basic idea is that a view controller is being popped when parent is nil.

Check out "Implementing a Container View Controller" for further details.


Since iOS 5 I've found that the easiest way of dealing with this situation is using the new method - (BOOL)isMovingFromParentViewController:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController) {
    // Do your stuff here
  }
}

- (BOOL)isMovingFromParentViewController makes sense when you are pushing and popping controllers in a navigation stack.

However, if you are presenting modal view controllers you should use - (BOOL)isBeingDismissed instead:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isBeingDismissed) {
    // Do your stuff here
  }
}

As noted in this question, you could combine both properties:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController || self.isBeingDismissed) {
    // Do your stuff here
  }
}

Other solutions rely on the existence of a UINavigationBar. Instead like my approach more because it decouples the required tasks to perform from the action that triggered the event, i.e. pressing a back button.

101
votes

While viewWillAppear() and viewDidDisappear() are called when the back button is tapped, they are also called at other times. See end of answer for more on that.

Using UIViewController.parent

Detecting the back button is better done when the VC is removed from it's parent (the NavigationController) with the help of willMoveToParentViewController(_:) OR didMoveToParentViewController()

If parent is nil, the view controller is being popped off the navigation stack and dismissed. If parent is not nil, it is being added to the stack and presented.

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

Swap out willMove for didMove and check self.parent to do work after the view controller is dismissed.

Stopping the dismiss

Do note, checking the parent doesn't allow you to "pause" the transition if you need to do some sort of async save. To do that you could implement the following. Only downside here is you lose the fancy iOS styled/animated back button. Also be careful here with the interactive swipe gesture. Use the following to handle this case.

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


More on view will/did appear

If you didn't get the viewWillAppear viewDidDisappear issue, Let's run through an example. Say you have three view controllers:

  1. ListVC: A table view of things
  2. DetailVC: Details about a thing
  3. SettingsVC: Some options for a thing

Lets follow the calls on the detailVC as you go from the listVC to settingsVC and back to listVC

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

And as we go back...
Settings > Detail (pop settingsVC) Detail.viewDidAppear <- appear
Detail > List (pop detailVC) Detail.viewDidDisappear <- disappear

Notice that viewDidDisappear is called multiple times, not only when going back, but also when going forward. For a quick operation that may be desired, but for a more complex operation like a network call to save, it may not.

16
votes

First Method

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

Second Method

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

Those who claim that this doesn't work are mistaken:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if self.isMovingFromParent {
        print("we are being popped")
    }
}

That works fine. So what is causing the widespread myth that it doesn’t?

The problem seems to be due to an incorrect implementation of a different method, namely that the implementation of willMove(toParent:) forgot to call super.

If you implement willMove(toParent:) without calling super, then self.isMovingFromParent will be false and the use of viewWillDisappear will appear to fail. It didn't fail; you broke it.

NOTE: The real problem is usually the second view controller detecting that the first view controller was popped. Please see also the more general discussion here: Unified UIViewController "became frontmost" detection?

EDIT A comment suggests that this should be viewDidDisappear rather than viewWillDisappear.

9
votes

I've playing (or fighting) with this problem for two days. IMO the best approach is just to create an extension class and a protocol, like this:

@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

This works because UINavigationController will receive a call to navigationBar:shouldPopItem: every time a view controller is popped. There we detect if back was pressed or not (any other button). The only thing you have to do is implement the protocol in the view controller where back is pressed.

Remember to manually pop the view controller inside backButtonPressedSel, if everything is ok.

If you already have subclassed UINavigationViewController and implemented navigationBar:shouldPopItem: don't worry, this won't interfere with it.

You may also be interested in disable the back gesture.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
8
votes

This works for me in iOS 9.3.x with Swift:

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

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

Unlike other solutions here, this doesn't seem to trigger unexpectedly.

4
votes

For the record, I think this is more of what he was looking for…

    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];
    }
4
votes

You can use the back button callback, like this:

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

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

for swift version you can do something like in global scope

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

Below one you put in the viewcontroller where you want to control back button action:

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

    return true
}
3
votes

The best way is to use the UINavigationController delegate methods

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

Using this you can know what controller is showing the UINavigationController.

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}
1
votes

You should check out the UINavigationBarDelegate Protocol. In this case you might want to use the navigationBar:shouldPopItem: method.

1
votes

As Coli88 said, you should check the UINavigationBarDelegate protocol.

In a more general way, you can also use the - (void)viewWillDisapear:(BOOL)animated to perform custom work when the view retained by the currently visible view controller is about to disappear. Unfortunately, this would cover bother the push and the pop cases.

1
votes

As purrrminator says, the answer by elitalon is not completely right, since your stuff would be executed even when popping the controller programmatically.

The solution I have found so far is not very nice, but it works for me. Besides what elitalon said, I also check whether I'm popping programmatically or not:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

You have to add that property to your controller and set it to YES before popping programmatically:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

Thanks for your help!

1
votes

For Swift with a UINavigationController:

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

I have solved this problem by adding a UIControl to the navigationBar on the left side .

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

And you need to remember to remove it when view will disappear:

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.leftItemControl) {
        [self.leftItemControl removeFromSuperview];
    }    
}

That's all!

1
votes

7ynk3r's answer was really close to what I did use in the end but it needed some tweaks:

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

self.navigationController.isMovingFromParentViewController is not working anymore on iOS8 and 9 I use :

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}
0
votes

I used Pedro Magalhães solution, except navigationBar:shouldPop was not called when I used it in an extension like this:

extension UINavigationController: UINavigationBarDelegate {
 public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
      return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}

But the same thing in a UINavigationController subclass worked fine.

class NavigationController: UINavigationController, UINavigationBarDelegate {

func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
    return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}

I see some other questions reporting this method not being called (but the other delegate methods being called as expected), from iOS 13?

iOS 13 and UINavigationBarDelegate::shouldPop()

-1
votes

(SWIFT)

finaly found solution.. method we were looking for is "willShowViewController" which is delegate method of 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
        }
    }
}