back button callback in navigationController in iOS
William Jockusch's answer solve this problem with easy trick.
-(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];
}
In my opinion the best solution.
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (![parent isEqual:self.parentViewController]) {
NSLog(@"Back pressed");
}
}
But it only works with iOS5+
it's probably better to override the backbutton so you can handle the event before the view is popped for things such as user confirmation.
in viewDidLoad create a UIBarButtonItem and set self.navigationItem.leftBarButtonItem to it passing in a 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];
}
Then you can do things like raise an UIAlertView to confirm the action, then pop the view controller, etc.
Or instead of creating a new backbutton, you can conform to the UINavigationController delegate methods to do actions when the back button is pressed.
Maybe it's a little too late, but I also wanted the same behavior before. And the solution I went with works quite well in one of the apps currently on the App Store. Since I haven't seen anyone goes with similar method, I would like to share it here. The downside of this solution is that it requires subclassing UINavigationController
. Though using Method Swizzling might help avoiding that, I didn't go that far.
So, the default back button is actually managed by UINavigationBar
. When a user taps on the back button, UINavigationBar
ask its delegate if it should pop the top UINavigationItem
by calling navigationBar(_:shouldPop:)
. UINavigationController
actually implement this, but it doesn't publicly declare that it adopts UINavigationBarDelegate
(why!?). To intercept this event, create a subclass of UINavigationController
, declare its conformance to UINavigationBarDelegate
and implement navigationBar(_:shouldPop:)
. Return true
if the top item should be popped. Return false
if it should stay.
There are two problems. The first is that you must call the UINavigationController
version of navigationBar(_:shouldPop:)
at some point. But UINavigationBarController
doesn't publicly declare it conformance to UINavigationBarDelegate
, trying to call it will result in a compile time error. The solution I went with is to use Objective-C runtime to get the implementation directly and call it. Please let me know if anyone has a better solution.
The other problem is that navigationBar(_:shouldPop:)
is called first follows by popViewController(animated:)
if the user taps on the back button. The order reverses if the view controller is popped by calling popViewController(animated:)
. In this case, I use a boolean to detect if popViewController(animated:)
is called before navigationBar(_:shouldPop:)
which mean that the user has tapped on the back button.
Also, I make an extension of UIViewController
to let the navigation controller ask the view controller if it should be popped if the user taps on the back button. View controllers can return false
and do any necessary actions and call popViewController(animated:)
later.
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
}
}
And in you view controllers, implement shouldBePopped(_:)
. If you don't implement this method, the default behavior will be to pop the view controller as soon as the user taps on the back button just like normal.
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
}
}
You can look at my demo here.