146

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?

ewok
  • 20,148
  • 51
  • 149
  • 254
  • possible duplicate of [Setting action for back button in navigation controller](http://stackoverflow.com/questions/1214965/setting-action-for-back-button-in-navigation-controller) – nielsbot Jan 08 '14 at 19:20
  • 1
    Look at the [solution in this thread](https://stackoverflow.com/questions/50750456/exception-cannot-manually-set-the-delegate-on-a-uinavigationbar-managed-by-a-co/50925934#50925934) – Jiri Volejnik Jun 19 '18 at 11:00
  • I did it this way [show decision here](https://stackoverflow.com/a/57137294/11079607) – Taras Jul 21 '19 at 22:08

18 Answers18

323

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
elitalon
  • 9,191
  • 10
  • 50
  • 86
  • I like you answer. But why did you use 'self.isBeingDismissed'? In my case, the statements in 'self.isBeingDismissed' do not get implemented. – Rutvij Kotecha Jan 12 '13 at 05:27
  • @Rut `- (BOOL)isBeingDismissed` only makes sense when your view controller is presented and **dismissed** as a modal view controller. See this question for further information: http://stackoverflow.com/q/10248412/592454 – elitalon Jan 12 '13 at 11:36
  • You say it's a method, but your code treats it as a property. Is there some equivalence in Objective-C such that 'if (self.isMovingFromParentViewController)' is treated as 'if ([self isMovingFromParentViewController])'? – SteveCaine Mar 13 '13 at 16:47
  • @SteveCaine Yes, it's a compiler feature that translates the dot notation in a message. It's a controversial topic, because some claim this practice to be evil. See http://stackoverflow.com/a/1249479/592454 and http://stackoverflow.com/a/2375991/592454 – elitalon Mar 13 '13 at 17:28
  • self.isMovingFromParentViewController appears to be false – Sam Nov 15 '13 at 11:31
  • @Sam Make sure you are calling `self.isMovingFromParentViewController` inside the correct methods. As stated in documentation _"This method returns YES only when called from inside the following methods:"_ `viewWillDisappear:` and `viewDidDisappear:`. Also make sure that you are using a navigation controller, not presenting modal view controllers. – elitalon Nov 15 '13 at 11:34
  • sorry, still no luck. I'm checking the value of isMovingFromParentViewController using LLDB aswell and it's no... – Sam Nov 15 '13 at 11:45
  • strange. on revisiting this it seems to be playing ball. – Sam Nov 28 '13 at 18:37
  • The only problem you can't use it if you want detect viewWillAppear while tab bar button press (: But it is very common case to separate navigation pop to root view controller from tab bar navigation. – malex Jan 20 '14 at 06:41
  • 4
    `self.isMovingFromParentViewController` has TRUE value when I'm poping the navigation stack programmatically using `popToRootViewControllerAnimated` - without any touch on the back button. Should I downvote your answer? (the subject says "'back' button is pressed on a navbar") – kas-kad Jan 23 '14 at 11:52
  • Didn't worked for me in iOS 8(.1). The view controller is in a navigation controller and the back button is pressed. Both boolean are false. It is in the correct method (`viewWillDisappear`). On iOS 7(.1) it worked. Perhaps it will work if you manually dismiss the view controller, but I use the tap of the user and the default back function. Now I'm using [this solution here](http://stackoverflow.com/a/17384144/426227). – testing Nov 25 '14 at 11:50
  • 2
    Terrific answer, thank you very much. In Swift I used: `override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }` – Camillo Mar 01 '15 at 16:22
  • working on iOS7 but not on iOS8 .... prefere : if (self.navigationController.topViewController != self) { NSLog(@"is Poping") } – Vassily Feb 15 '16 at 13:25
  • For me, it was `isMovingToParentViewController` that worked. – elquimista Nov 14 '16 at 11:26
  • 2
    You should only do this within `-viewDidDisappear:` since it's possible that you'll get a `-viewWillDisappear:` without a `-viewDidDisappear:` (like when you start swiping to dismiss a navigation controller item and then cancel that swipe. – Heath Borders Jun 09 '17 at 21:22
  • 3
    Looks like not a reliable solution any more. Worked at the time I first used this (it was iOS 10). But now I accidentally found it calmly stopped working (iOS 11). Had to switch to the "willMove(toParentViewController)" solution. – Vitalii Jan 24 '18 at 14:27
  • It’s not out of date and it works fine under all scenarios. I don’t understand why you want to retract a correct answer. – matt Jul 18 '18 at 10:34
  • Thanks @matt. I've update the update (!) to better reflect the motivation for the alternative solution. – elitalon Jul 19 '18 at 06:37
  • I want to confirm that solution works (self.movingFromParentViewController variant) as of today and as of iOS 12. Thanks @elitalon – Nick May 23 '19 at 15:52
105

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 its 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.

clearlight
  • 12,255
  • 11
  • 57
  • 75
WCByrne
  • 1,549
  • 1
  • 11
  • 16
  • Just a note, user `didMoveToParantViewController:` to do work when the view is no longer visible. Helpful for iOS7 with the interactiveGesutre – WCByrne May 02 '14 at 17:48
  • didMoveToParentViewController* there is a typo – thewormsterror Aug 08 '14 at 09:50
  • Don't forget to call [super willMoveToParentViewController:parent]! – ScottyB Oct 18 '14 at 01:18
  • 2
    The parent parameter is nil when you are popping to the parent view controller, and non-nil when the view this method appears in is being shown. You can use that fact to do an action only when the Back button is pressed, and not when arriving at the view. That was, after all, the original question. :) – Mike Mar 16 '15 at 22:26
  • 1
    This also gets called when programmatically using `_ = self.navigationController?.popViewController(animated: true)`, so it's not just called on a Back button press. I am looking for a call that works *only* when Back is pressed. – Ethan Allen Apr 17 '18 at 17:00
  • @EthanAllen The only way I can think of is the option under Stopping the dismiss – WCByrne Apr 19 '18 at 01:12
20

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 3
    This code is executed when the back button is tapped, but is also executed if the VC is popped programmatically. – biomiker Nov 27 '18 at 21:34
  • 2
    @biomiker Sure, but that would also be true of the other approaches. Popping is popping. The question is how to detect a pop when you _didn't_ pop programatically. If you pop programatically you already _know_ you are popping so there is nothing to detect. – matt Nov 27 '18 at 21:41
  • 1
    Yes, this is true of several of the other approaches and many of those have similar comments. I was just clarifying since this was a recent answer with a specific rebuttal and I had gotten my hopes up when I read it. For the record though, the question is how to detect a press of the back button. It's a reasonable argument to say that code that will also execute in situations where the back button is not pressed, without indicating whether or not the back button was pressed, does not fully solve the real question, even if perhaps the question could have been more explicit on that point. – biomiker Nov 27 '18 at 23:20
  • I think the main issue is that this method is also executed when another view controller is stacked on top of the current one – FredFlinstone Dec 06 '19 at 12:57
  • 6
    Unfortunately this returns `true` for the interactive swipe pop gesture - from the left edge of the view controller - even if the swipe didn't fully pop it. So instead of checking it in `willDisappear`, doing so in `didDisappear` works. – badhanganesh Jul 13 '20 at 18:11
  • 2
    @badhanganesh Thanks, edited answer to include that info. – matt Jul 13 '20 at 19:17
  • @MycroftCanner You're just repeating what was already discussed in the previous comments and added to the answer. – matt May 07 '21 at 19:34
  • @badhanganesh That would seem to be a bug in iOS. – clearlight May 31 '22 at 22:04
16

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];
}
Zar E Ahmer
  • 33,936
  • 20
  • 234
  • 300
  • 1
    Second method was the only one that worked for me. First method was also called upon my view being presented, which wasn't acceptable for my use case. – marcshilling Mar 23 '15 at 21:36
9

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;
}
7ynk3r
  • 958
  • 13
  • 17
  • 1
    This answer was almost complete for me, except that I found that 2 viewcontrollers would often be popped. Returning YES causes the calling method to call pop, so calling pop as well meant that 2 viewcontrollers would be popped. See this answer on another question for more deets (a very good answer that deserves more upvotes): http://stackoverflow.com/a/26084150/978083 – Jason Ridge Jul 30 '15 at 16:39
  • Good point, my description was not clear about that fact. The "Remember to manually pop the view controller if everything is ok" it's only for the case of returning "NO", otherwise the flow is the normal pop. – 7ynk3r Jul 30 '15 at 18:31
  • 1
    For "else' branch, It's better to call super implementation if you don't want to handle pop yourself and let it return whatever it thinks is right, which is mostly YES, but it also takes care of pop itself then and animates chevron properly. – pronebird Aug 11 '16 at 11:27
8

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.

Chris Villa
  • 3,901
  • 1
  • 18
  • 10
  • it is better to use willMove instead – Eugene Gordin Sep 14 '17 at 06:57
  • Not sure about `willMove` as it might have the same problem as `willDisappear`: he user can start dismissing the view controller with a swipe, `willDisappear` will be called but the user can still cancel the swipe! – Mycroft Canner May 07 '21 at 19:32
7

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
}
Avinash
  • 4,304
  • 1
  • 23
  • 18
  • 2
    Don't know why someone down voted. This seems to be by far best answer. – Avinash Apr 11 '19 at 12:07
  • @Avinash Where does `navigationShouldPopOnBackButton` come from? It is not part of the public API. – elitalon May 29 '19 at 08:11
  • @elitalon Sorry, this was half answer. I had thought remaining context was there in question. Anyway have updated the answer now – Avinash Jun 12 '19 at 09:10
  • I agree. This is an underrated solution that uses the system back button with the "<" and the back menu. I always prefer to feed my code into the system callbacks where possible instead of imitating UI elements. – RyuX51 Apr 01 '21 at 09:51
4

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];
    }
Paul Brady
  • 503
  • 4
  • 10
  • 1
    Thanks Paul, this solution is quite simple. Unfortunately, the icon is different. This is the "rewind" icon, not the back icon. Maybe there's a way to use the back icon... – Ferran Maylinch May 11 '15 at 11:01
3

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");
}
Harald
  • 422
  • 5
  • 10
  • This should be marked as the correct answer! Might also want to add one more line just to remind folks --> self.navigationController.delegate = self; – Mike Critchley Dec 06 '17 at 20:22
2

For Swift with a UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
  • 8,454
  • 4
  • 47
  • 48
2

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

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
Chris Pawley
  • 21
  • 1
  • 4
1

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!

Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100
1

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!

Eric
  • 21
  • 1
  • 4
1

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;
    }
}
Community
  • 1
  • 1
micromanc3r
  • 569
  • 1
  • 8
  • 16
1

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

Coli88
  • 302
  • 2
  • 9
1

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.

ramdam
  • 9
  • 1
0

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
    }
}
Vassily
  • 899
  • 2
  • 8
  • 19
-1

(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
        }
    }
}
Jiří Zahálka
  • 8,070
  • 2
  • 21
  • 17