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?
This question is related to
iphone
objective-c
ios
xcode
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;
}
}
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.
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
}
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
}
}
}
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];
}
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!
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.
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.
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
}
}
}
If you didn't get the viewWillAppear
viewDidDisappear
issue, Let's run through an example. Say you have three view controllers:
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.
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");
}
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];
}
You should check out the UINavigationBarDelegate Protocol. In this case you might want to use the navigationBar:shouldPopItem: method.
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
}
}
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;
}
For Swift with a UINavigationController:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
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
.
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!
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.
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.
Source: Stackoverflow.com