Stopping the self.navigationItem.leftBarButtonItem from exiting a view [duplicate] - iphone
I'm trying to overwrite the default action of the back button in a navigation controller. I've provided a target an action on the custom button. The odd thing is when assigning it though the backbutton attribute it doesn't pay attention to them and it just pops the current view and goes back to the root:
UIBarButtonItem *backButton = [[UIBarButtonItem alloc]
initWithTitle: #"Servers"
style:UIBarButtonItemStylePlain
target:self
action:#selector(home)];
self.navigationItem.backBarButtonItem = backButton;
As soon as I set it through the leftBarButtonItem on the navigationItem it calls my action, however then the button looks like a plain round one instead of the arrowed back one:
self.navigationItem.leftBarButtonItem = backButton;
How can I get it to call my custom action before going back to the root view? Is there a way to overwrite the default back action, or is there a method that is always called when leaving a view (viewDidUnload doesn't do that)?
Try putting this into the view controller where you want to detect the press:
-(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];
}
I've implemented UIViewController-BackButtonHandler extension. It does not need to subclass anything, just put it into your project and override navigationShouldPopOnBackButton method in UIViewController class:
-(BOOL) navigationShouldPopOnBackButton {
if(needsShowConfirmation) {
// Show confirmation alert
// ...
return NO; // Ignore 'Back' button this time
}
return YES; // Process 'Back' button click and pop view controller
}
Download sample app.
Unlike Amagrammer said, it's possible. You have to subclass your navigationController. I explained everything here (including example code).
Swift Version:
(of https://stackoverflow.com/a/19132881/826435)
In your view controller you just conform to a protocol and perform whatever action you need:
extension MyViewController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool {
performSomeActionOnThePressOfABackButton()
return false
}
}
Then create a class, say NavigationController+BackButton, and just copy-paste the code below:
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool
}
extension UINavigationController {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
// Prevents from a synchronization issue of popping too many navigation items
// and not enough view controllers or viceversa from unusual tapping
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
var shouldPop = true
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
shouldPop = viewController.shouldPopOnBackButtonPress()
}
if (shouldPop) {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
// Prevent the back button from staying in an disabled state
for view in navigationBar.subviews {
if view.alpha < 1.0 {
UIView.animate(withDuration: 0.25, animations: {
view.alpha = 1.0
})
}
}
}
return false
}
}
It isn't possible to do directly. There are a couple alternatives:
Create your own custom UIBarButtonItem that validates on tap and pops if the test passes
Validate the form field contents using a UITextField delegate method, such as -textFieldShouldReturn:, which is called after the Return or Done button is pressed on the keyboard
The downside of the first option is that the left-pointing-arrow style of the back button cannot be accessed from a custom bar button. So you have to use an image or go with a regular style button.
The second option is nice because you get the text field back in the delegate method, so you can target your validation logic to the specific text field sent to the delegate call-back method.
For some threading reasons, the solution mentionned by #HansPinckaers wasn't right for me, but I found a very easier way to catch a touch on the back button, and I wanna pin this down here in case this could avoid hours of deceptions for someone else.
The trick is really easy : just add a transparent UIButton as a subview to your UINavigationBar, and set your selectors for him as if it was the real button!
Here's an example using Monotouch and C#, but the translation to objective-c shouldn't be too hard to find.
public class Test : UIViewController {
public override void ViewDidLoad() {
UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
b.BackgroundColor = UIColor.Clear; //making the background invisible
b.Title = string.Empty; // and no need to write anything
b.TouchDown += delegate {
Console.WriteLine("caught!");
if (true) // check what you want here
NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
};
NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
}
}
Fun fact : for testing purposes and to find good dimensions for my fake button, I set its background color to blue... And it shows behind the back button! Anyway, it still catches any touch targetting the original button.
Overriding navigationBar(_ navigationBar:shouldPop): This is not a good idea, even if it works. for me it generated random crashes on navigating back. I advise you to just override the back button by removing the default backButton from navigationItem and creating a custom back button like below:
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction)
...
}
========================================
Building on previous responses with UIAlert in Swift5 in a Asynchronous way
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ())
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
viewController.shouldPopOnBackButtonPress { shouldPop in
if (shouldPop) {
/// on confirm => pop
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
/// on cancel => do nothing
}
}
/// return false => so navigator will cancel the popBack
/// until user confirm or cancel
return false
}else{
DispatchQueue.main.async {
self.popViewController(animated: true)
}
}
return true
}
}
On your controller
extension MyController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ()) {
let msg = "message"
/// show UIAlert
alertAttention(msg: msg, actions: [
.init(title: "Continuer", style: .destructive, handler: { _ in
completion(true)
}),
.init(title: "Annuler", style: .cancel, handler: { _ in
completion(false)
})
])
}
}
This technique allows you to change the text of the "back" button without affecting the title of any of the view controllers or seeing the back button text change during the animation.
Add this to the init method in the calling view controller:
UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];
temporaryBarButtonItem.title = #"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
Easiest way
You can use the UINavigationController's delegate methods. The method willShowViewController is called when the back button of your VC is pressed.do whatever you want when back btn pressed
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Here's my Swift solution. In your subclass of UIViewController, override the navigationShouldPopOnBackButton method.
extension UIViewController {
func navigationShouldPopOnBackButton() -> Bool {
return true
}
}
extension UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
if let vc = self.topViewController {
if vc.navigationShouldPopOnBackButton() {
self.popViewControllerAnimated(true)
} else {
for it in navigationBar.subviews {
let view = it as! UIView
if view.alpha < 1.0 {
[UIView .animateWithDuration(0.25, animations: { () -> Void in
view.alpha = 1.0
})]
}
}
return false
}
}
return true
}
}
Found a solution which retains the back button style as well.
Add the following method to your view controller.
-(void) overrideBack{
UIButton *transparentButton = [[UIButton alloc] init];
[transparentButton setFrame:CGRectMake(0,0, 50, 40)];
[transparentButton setBackgroundColor:[UIColor clearColor]];
[transparentButton addTarget:self action:#selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
[self.navigationController.navigationBar addSubview:transparentButton];
}
Now provide a functionality as needed in the following method:
-(void)backAction:(UIBarButtonItem *)sender {
//Your functionality
}
All it does is to cover the back button with a transparent button ;)
I don't believe this is possible, easily. The only way I believe to get around this is to make your own back button arrow image to place up there. It was frustrating for me at first but I see why, for consistency's sake, it was left out.
You can get close (without the arrow) by creating a regular button and hiding the default back button:
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
There's an easier way by just subclassing the delegate method of the UINavigationBar and override the ShouldPopItemmethod.
This approach worked for me (but the "Back" button will not have the "<" sign):
- (void)viewDidLoad
{
[super viewDidLoad];
UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:#"Back"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(backButtonClicked)];
self.navigationItem.leftBarButtonItem = backNavButton;
}
-(void)backButtonClicked
{
// Do something...
AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[delegate.navController popViewControllerAnimated:YES];
}
onegray's solution is not safe.According to the official documents by Apple,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html,
we should avoid doing that.
"If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes."
Using Swift:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
Here is Swift 3 version of #oneway's answer for catching navigation bar back button event before it gets fired. As UINavigationBarDelegate cannot be used for UIViewController, you need to create a delegate that will be triggered when navigationBar shouldPop is called.
#objc public protocol BackButtonDelegate {
#objc optional func navigationShouldPopOnBackButton() -> Bool
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < (navigationBar.items?.count)! {
return true
}
var shouldPop = true
let vc = self.topViewController
if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
shouldPop = vc.navigationShouldPopOnBackButton()
}
if shouldPop {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
for subView in navigationBar.subviews {
if(0 < subView.alpha && subView.alpha < 1) {
UIView.animate(withDuration: 0.25, animations: {
subView.alpha = 1
})
}
}
}
return false
}
}
And then, in your view controller add the delegate function:
class BaseVC: UIViewController, BackButtonDelegate {
func navigationShouldPopOnBackButton() -> Bool {
if ... {
return true
} else {
return false
}
}
}
I've realised that we often want to add an alert controller for users to decide whether they wanna go back. If so, you can always return false in navigationShouldPopOnBackButton() function and close your view controller by doing something like this:
func navigationShouldPopOnBackButton() -> Bool {
let alert = UIAlertController(title: "Warning",
message: "Do you want to quit?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
present(alert, animated: true, completion: nil)
return false
}
func yes() {
print("yes")
DispatchQueue.main.async {
_ = self.navigationController?.popViewController(animated: true)
}
}
func no() {
print("no")
}
Swift 4 iOS 11.3 Version:
This builds on the answer from kgaidis from https://stackoverflow.com/a/34343418/4316579
I am not sure when the extension stopped working, but at the time of this writing (Swift 4), it appears that the extension will no longer be executed unless you declare UINavigationBarDelegate conformity as described below.
Hope this helps people that are wondering why their extension no longer works.
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
}
}
By using the target and action variables that you are currently leaving 'nil', you should be able to wire your save-dialogs in so that they are called when the button is "selected". Watch out, this may get triggered at strange moments.
I agree mostly with Amagrammer, but I don't think it would be that hard to make the button with the arrow custom. I would just rename the back button, take a screen shot, photoshop the button size needed, and have that be the image on the top of your button.
You can try accessing the NavigationBars Right Button item and set its selector property...heres a reference UIBarButtonItem reference, another thing if this doenst work that will def work is, set the right button item of the nav bar to a custom UIBarButtonItem that you create and set its selector...hope this helps
For a form that requires user input like this, I would recommend invoking it as a "modal" instead of part of your navigation stack. That way they have to take care of business on the form, then you can validate it and dismiss it using a custom button. You can even design a nav bar that looks the same as the rest of your app but gives you more control.
To intercept the Back button, simply cover it with a transparent UIControl and intercept the touches.
#interface MyViewController : UIViewController
{
UIControl *backCover;
BOOL inhibitBackButtonBOOL;
}
#end
#implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Cover the back button (cannot do this in viewWillAppear -- too soon)
if ( backCover == nil ) {
backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
// show the cover for testing
backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
[backCover addTarget:self action:#selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
UINavigationBar *navBar = self.navigationController.navigationBar;
[navBar addSubview:backCover];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[backCover removeFromSuperview];
backCover = nil;
}
- (void)backCoverAction
{
if ( inhibitBackButtonBOOL ) {
NSLog(#"Back button aborted");
// notify the user why...
} else {
[self.navigationController popViewControllerAnimated:YES]; // "Back"
}
}
#end
At least in Xcode 5, there is a simple and pretty good (not perfect) solution. In IB, drag a Bar Button Item off the Utilities pane and drop it on the left side of the Navigation Bar where the Back button would be. Set the label to "Back." You will have a functioning button that you can tie to your IBAction and close your viewController. I'm doing some work and then triggering an unwind segue and it works perfectly.
What isn't ideal is that this button does not get the < arrow and does not carry forward the previous VCs title, but I think this can be managed. For my purposes, I set the new Back button to be a "Done" button so it's purpose is clear.
You also end up with two Back buttons in the IB navigator, but it is easy enough to label it for clarity.
Swift
override func viewWillDisappear(animated: Bool) {
let viewControllers = self.navigationController?.viewControllers!
if indexOfArray(viewControllers!, searchObject: self) == nil {
// do something
}
super.viewWillDisappear(animated)
}
func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
for (index, value) in enumerate(array) {
if value as UIViewController == searchObject as UIViewController {
return index
}
}
return nil
}
Found new way to do it :
Objective-C
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == NULL) {
NSLog(#"Back Pressed");
}
}
Swift
override func didMoveToParentViewController(parent: UIViewController?) {
if parent == nil {
println("Back Pressed")
}
}
Swift version of #onegray's answer
protocol RequestsNavigationPopVerification {
var confirmationTitle: String { get }
var confirmationMessage: String { get }
}
extension RequestsNavigationPopVerification where Self: UIViewController {
var confirmationTitle: String {
return "Go back?"
}
var confirmationMessage: String {
return "Are you sure?"
}
}
final class NavigationController: UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
popViewControllerAnimated(true)
return true
}
let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
dispatch_async(dispatch_get_main_queue(), {
let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
UIView.animateWithDuration(0.25) {
dimmed.forEach { $0.alpha = 1 }
}
})
return
})
alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
dispatch_async(dispatch_get_main_queue(), {
self.popViewControllerAnimated(true)
})
})
presentViewController(alertController, animated: true, completion: nil)
return false
}
}
Now in any controller, just conform to RequestsNavigationPopVerification and this behaviour is adopted by default.
Use isMovingFromParentViewController
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
// current viewController is removed from parent
// do some work
}
}
The answer from #William is correct however, if the user starts a swipe-to-go-back gesture the viewWillDisappear method is called and even self won't be in the navigation stack (that is, self.navigationController.viewControllers won't contain self), even if the swipe is not completed and the view controller is not actually popped. Thus, the solution would be to:
Disable the swipe-to-go-back gesture in viewDidAppear and only allow using the back button, by using:
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)])
{
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Or simply use viewDidDisappear instead, as follows:
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if (![self.navigationController.viewControllers containsObject:self])
{
// back button was pressed or the the swipe-to-go-back gesture was
// completed. We know this is true because self is no longer
// in the navigation stack.
}
}
The solution I have found so far is not very nice, but it works for me. Taking this answer, 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];
Related
Remove any view from any where e.g from window
I have 2 views on the screen, one is overlayView at the bottom and introView at the top of that overlayView. When I tap(tapToContinueAction) on the screen they should both hide or remove themselves. extension UIView { .... func hideView(view: UIView, hidden: Bool) { UIView.transition(with: view, duration: 0.5, options: .transitionCrossDissolve, animations: { view.isHidden = hidden }) } } class IntroScreen #IBAction func tapToContinueAction(_ sender: UITapGestureRecognizer) { self.hideView(view: self, hidden: true) } -- class OverlayView : UiView { ... } In current situation i can hide introScreen only and i dont know how the other class's action can effect the overlayView at the same time and hide that view as well. Any idea?
You have two different classes for your views. Make an extension of your window to remove your specific views just like I have made removeOverlay and removeIntroView both these computed properties will go and search in subviews list of window and check each view with their type and remove them. Thats how you can remove any view form any where. class OverLayView: UIView {} class IntroView: UIView { #IBAction func didTapYourCustomButton(sender: UIButton) { let window = (UIApplication.shared.delegate as! AppDelegate).window! window.removeOverlay window.removeIntroView } } extension UIWindow { var removeOverlay: Void { for subview in self.subviews { if subview is OverLayView { subview.removeFromSuperview()// here you are removing the view. subview.hidden = true// you can hide the view using this } } } var removeIntroView: Void { for subview in self.subviews { if subview is IntroView { subview.removeFromSuperview()// here you are removing the view. subview.hidden = true// you can hide the view using this } } } }
How to slide TabBar up to hide it in tvOS app?
In my tvOS app I have a TabBarController with 3 viewControllers. What I want to do is to automatically hide/change focus of the tabBar when I switch to the next viewController. I saw some posts here, on SO that suggested to change alfa on the tabBar, but I would like to have a slide up animation, same way as it does when you change focus to something in the viewController. Any kind of help is highly appreciated.
As Charles said.. Something like this in the derived UITabBarController: var focusOnChildVC : Bool = false { didSet { self.setNeedsFocusUpdate() } }; override weak var preferredFocusedView: UIView? { get { let v : UIView?; let focused = UIScreen.mainScreen().focusedView //A bit of a hack but seems to work for picking up whether the VC is active or not if (focusOnChildVC && focused != nil) { v = self.selectedViewController?.preferredFocusedView } else { //If we are focused on the main VC and then clear out of property as we're done with overriding the focus now if (focusOnChildVC) { focusOnChildVC = false } v = super.preferredFocusedView; } return v } }
The basic idea of the solution described below is to subclass UITabBarController and selectively use the super implementation of weak var preferredFocusedView: UIView? { get } or one that returns selectedViewController?.preferredFocusView along with an implementation of didUpdateFocusInContext(_:withAnimationCoordinator:) that sets up an NSTimer that triggers a focus update and sets a flag that controls the preferredFocusView implementation. More verbosely, Subclass UITabBarController and override didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator). In your implementation (make sure to call the super implementation) you can inspect the context and determine if a descendent view of the tabBar property is the nextFocusedView or the previousFocusedView (and the nextFocusedView is not a descendent). If the tab bar is gaining focus you can create an NSTimer for the duration that you want to show the tab bar before hiding it. If the tab bar loses focus before the timer fires, invalidate it. If the timer fires, call setNeedsFocusUpdate() followed by updateFocusIfNeeded(). The last piece you need to get this to work is a flag that is set to true while the timer is set. You then need to override weak var preferredFocusedView: UIView? { get } and call the super implementation if the flag is false and if it is true return selectedViewController?.preferredFocusedView.
You can do it in a UITabBarController subclass: final class TabBarViewController: UITabBarController { private(set) var isTabBarHidden = false func setTabBarHidden(_ isHidden: Bool, animated: Bool) { guard isTabBarHidden != isHidden else { return } var frame: CGRect let alpha: CGFloat if isHidden { frame = tabBar.frame frame.origin.y -= frame.height alpha = 0 } else { frame = tabBar.frame frame.origin.y += frame.height alpha = 1 } let animations = { self.tabBar.frame = frame self.tabBar.alpha = alpha } if animated { UIView.animate(withDuration: 0.3, animations: animations) } else { animations() } isTabBarHidden = isHidden } }
Detecting when the 'back' button is pressed on a navbar
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?
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.
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: ListVC: A table view of things DetailVC: Details about a thing 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.
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.
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]; }
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; }
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 }
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]; }
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"); }
For Swift with a UINavigationController: override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if self.navigationController?.topViewController != self { print("back button tapped") } }
You should check out the UINavigationBarDelegate Protocol. In this case you might want to use the navigationBar:shouldPopItem: method.
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.
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!
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!
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; } }
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()
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 } }
(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 } } }
When using hidesBottomBarWhenPushed, i want the tab bar to reappear when i push another view
I have a navigation controller. For one of the views i want to hide the bottom tab bar, so it gets the max possible screen real estate. To do this, i have: - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { self.hidesBottomBarWhenPushed = YES; // To hide the tab bar } return self; } But for the next view that i push on the stack, i want the tab bar to reappear. Is there a way to do this?
As of iOS5, there's a very easy means of accomplishing this. It's essentially the same method as Deepak, but there aren't any artifacts with the animation - everything looks as expected. On init, set self.hidesBottomBarWhenPushed = YES; just as you have above. When it's time to push the new controller on the stack, it's as simple as: self.hidesBottomBarWhenPushed = NO; UIViewController *controller = [[[BBListingController alloc] init] autorelease]; [self.navigationController pushViewController:controller]; self.hidesBottomBarWhenPushed = YES; It's important to reset the value to YES after the controller has been pushed in order to re-hide the bar when the user taps the Back button and the view comes back into view.
I'm have solved this problem like that: Almost all my ViewControllers are children of BaseViewController. So, example: class BaseVC: UIViewController { final override var hidesBottomBarWhenPushed: Bool { get { if navigationController?.viewControllers.last == self { return prefersBottomBarHidden ?? super.hidesBottomBarWhenPushed } else { return false } } set { super.hidesBottomBarWhenPushed = newValue } } private(set) var prefersBottomBarHidden: Bool? } Just override variable "prefersBottomBarHidden" in ViewController where BottomBar should be hidden: override var prefersBottomBarHidden: Bool? { return true }
It's been a while since this question was asked, but none of these answers address using Storyboard segues. It turns out to be pretty easy: override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "MyViewControllerIdentifier" { // Hide the tabbar during this segue hidesBottomBarWhenPushed = true // Restore the tabbar when it's popped in the future DispatchQueue.main.async { self.hidesBottomBarWhenPushed = false } } }
One can make it reappear but it will result in an incorrect animation. Page comes in left and the bottom bar right. So it is probably not the behavior you want. But in the same controller, do self.hidesBottomBarWhenPushed = NO; before pushing the next view controller in.
Case one: To hide UITabbarController in a cetain UIVIewController, for example while calling self.performSegueWithIdentifier("Identifier", sender: self), it is necesssary prior to to that, set self.hidesBottomBarWhenPushed = true flag. And after self.hidesBottomBarWhenPushed = false flag. But we have to understad that through one UIViewController, UITabbarController will re-appear and, in case if you need to use UITabbarController with single UIViewControler, it wont yield right result. in the FirstItemViewController #IBAction func pushToControllerAction(sender: AnyObject) { self.hidesBottomBarWhenPushed = true self.performSegueWithIdentifier("nextController", sender: self) self.hidesBottomBarWhenPushed = false } Case Two: To hide UITabbarController in a certain UIVIewController, after which a UITabbarController should be popped, it is necessary, for example, while calling self.performSegueWithIdentifier("nextController", sender: self) , to set self.hidesBottomBarWhenPushed = true before the method. Alse willMoveToParentViewController(parent: UIViewController?) in the method should be configured as it shown in the code example. in the first UIViewController "FirstItemViewController" #IBAction func pushToControllerAction(sender: AnyObject) { self.hidesBottomBarWhenPushed = true self.performSegueWithIdentifier("nextController", sender: self) } in the next UIViewController "ExampleViewController"` override func willMoveToParentViewController(parent: UIViewController?) { if parent == nil { var viewControllers = self.navigationController!.viewControllers if ((viewControllers[viewControllers.count - 2]).isKindOfClass(FirstItemViewController.self)) { (viewControllers[viewControllers.count - 2] as! FirstItemViewController).hidesBottomBarWhenPushed = false } } } Swift 3 code: let viewControllers = self.navigationController!.viewControllers if ((viewControllers[viewControllers.count - 2]) is (FirstItemViewController)) { (viewControllers[viewControllers.count - 2] as! FirstItemViewController).hidesBottomBarWhenPushed = false } Test project
In a root view controller "A" (which is showing the tabBar), when it comes time to show another view controller "B" where no tabBar is wanted: self.hidesBottomBarWhenPushed = YES; // hide the tabBar when pushing B [self.navigationController pushViewController:viewController_B animated:YES]; self.hidesBottomBarWhenPushed = NO; // for when coming Back to A In view controller B, when it comes time to show a third view controller C (tabBar wanted again): self.hidesBottomBarWhenPushed = NO; // show the tabbar when pushing C [self.navigationController pushViewController:viewController_C animated:YES]; self.hidesBottomBarWhenPushed = YES; // for when coming Back to B
UITableview edit/done button
I have a tableview and navigation bar on the top. I have a Edit button on the left of my navigation bar with the following line of code. self.navigationItem.leftBarButtonItem = self.editButtonItem; When i click on the edit button, it changes to done button. All is fine so far. Where do i add code, if i want to do a small operation when the Done button is clicked.?
The button stops committing the changes to your controller class once you override it's default action with self.editButtonItem.action = #selector(editClicked:); What you should do is override UIViewController's setEditing method in your own controller class: - (void)setEditing:(BOOL)editing animated:(BOOL)animated { [super setEditing:editing animated:animated]; if(editing == YES) { // Your code for entering edit mode goes here } else { // Your code for exiting edit mode goes here } } You also need to set your UIBarButtonItem to "Edit" in storyboard or if you prefer doing it in code use the following: - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.rightBarButtonItem = self.editButtonItem; } editButtonItem is a helper property already set by the system for your comfort.
Here is a Swift version I used: override func setEditing(editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) if editing { } else { } }
thats what i did on Swift 4: this is create bar button in viewDidLoad(): // init barbutton and set default to true self.navigationItem.rightBarButtonItem = self.editButtonItem super.isEditing = true add override setEditing() below the viewDidLoad(): override func setEditing (_ editing:Bool, animated:Bool) { super.setEditing(editing,animated:animated) if(self.isEditing) { self.editButtonItem.title = "Edit" }else { self.editButtonItem.title = "Done" } }