30

I have a UINavigationController (to use like a wizard page) which I create programmatically and I need to display a "Cancel" button to cancel the process in any UIViewController.

Creating the UINavigationController:

FirstVC *firstVC = [[[FirstVC alloc] initWithNibName:@"FirstPage" bundle:nil] autorelease];
firstVC.delegate = self;

navigationController = [[UINavigationController alloc] initWithRootViewController:firstVC];
[self.view addSubview:navigationController.view];

Adding Cancel Button:

UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelRequestNewLeave:)];
navigationController.topViewController.navigationItem.rightBarButtonItem = cancelButton;
[cancelButton release];

But when I push a second page to UINavigationController the cancel button is not shown on the UINavigationBar. If I go back to first page, the cancel button is there. So, apparently the button is added only for the first view. I believe this is because I'm not subclassing UINavigationController, because I need to use it in a subview. But I don't know how to set the rightBarButtonItem in a UINavigationController which is created programmatically.

navigationController.topViewController.navigationItem.rightBarButtonItem = cancelButton;

Can someone shed a light on this?

Thanks in advance.

Dharmesh Dhorajiya
  • 3,976
  • 9
  • 30
  • 39
Ozay
  • 443
  • 1
  • 6
  • 12

8 Answers8

43

The navigation item is per view controller. The navigation bar draws its contents from the navigation item of the view controller whose view it's currently framing, which corresponds to the view controller at the top of the navigation controller's stack.

You basically need each view controller to stick a cancel button in its navigation item. You can do any of the following:

  • Copy-paste the code into all relevant view controllers.
  • Move the code into a utility function or class and call that.
  • Create a common superclass for all relevant view controllers that handles setting up the cancel button for its subclasses.
Jeremy W. Sherman
  • 35,901
  • 5
  • 77
  • 111
  • 1
    Thanks for the clarification. I will go for the 3rd option and create a superclass. – Ozay Jun 18 '11 at 08:08
  • 2
    Although a late comment, you should avoid providing functionality to classes through a super class. Later in development you might have some controller subclasses that would not "like" to inherit the extra functionality. Not only that, your code could become more complex. – Don Miguel Jul 14 '14 at 16:08
  • Thanks for your explanation on the relationship between ViewControllers and NavigationItems. My Approach concerning custom buttons that survive segues (controller switches) will be like this: * ViewControllers inherit from a BaseViewController. * BaseViewController holds property of NSMutableDictionary * This Dictionary keeps info on Buttons that should show up in NavigationItem. * the BaseViewController updates the current NavigationItems button set. Maybe my BaseViewController will be wired by protocol. So it gets notified about any change of button configuration. – HBublitz Aug 18 '14 at 09:36
  • 1
    If you can't have one common superclass because you inherit from different types of UIViewControllers within UIKit, such as UICollectionViewController and UIViewController, two superclasses would be needed. Instead, method swizzling viewDidLoad and placing the initialization code there becomes a viable option. – kev Sep 03 '15 at 00:42
  • how we will call the actions of subclass when a navigationitem button is pressed? – hariszaman Aug 29 '16 at 10:57
  • 1
    @hariszaman That sounds like a new question: https://stackoverflow.com/questions/ask – Jeremy W. Sherman Aug 31 '16 at 15:42
23

You can also subclass UINavigationcontroller and overide few methods like this:

- (id)initWithRootViewController:(UIViewController *)rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        [self setCloseButtonToController:rootViewController];
    }
    return self;
}

- (void)dismissController {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)setCloseButtonToController:(UIViewController *)viewController {
    UIBarButtonItem *closeItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(dismissController)];
    [viewController.navigationItem setRightBarButtonItem:closeItem];
}

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [super pushViewController:viewController animated:animated];

    [self setCloseButtonToController:viewController];

}
Rafał Wójcik
  • 591
  • 3
  • 13
  • 2
    Thank you. I'm presenting a `UINavigationController` modally and I wanted to have a simple Cancel button to apply to all screens within it. Apparently my use case is extremely hard to Google and not a popular one. – ray Apr 20 '15 at 23:07
  • 1
    I like your style! :) – Artem Zaytsev May 04 '15 at 08:49
  • And with sotryBoard how to set the `willShowViewController` to display correctly the button ? – Chlebta Jun 17 '15 at 09:43
  • check this please : http://stackoverflow.com/questions/30372135/add-buttons-to-uinavigationcontroller-subclass – Chlebta Jun 17 '15 at 09:57
7

You can instead adopt the UINavigationControllerDelegate protocol in the class which creates the UINavigationController instance. You can also create the cancelButton in advance and then implement navigationController:willShowViewController:animated: like this,

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    viewController.navigationItem.rightBarButtonItem = cancelButton;
}

You will have to remember to create and hold the cancelButton and not release it. This will also mean cancelRequestNewLeave: will have to be a method in class that creates the UINavigationController instance which is what it is right now I guess.

Deepak Danduprolu
  • 44,595
  • 12
  • 101
  • 105
  • I'm not sure it's a good idea to put the same BBI into multiple bars (the iPad popover APIs let you show a view from a BBI, so presumably it should only be on screen in one place at a time; I'm not sure if BBIs magically contain references to their parents) – tc. Jun 18 '11 at 00:09
  • Using `willShowViewController` to **set left button** in parent view will override the back button in all navigation controller and when you push viewController the back button will not appear any way to fix this issue : http://stackoverflow.com/questions/30372135/add-buttons-to-uinavigationcontroller-subclass – Chlebta Jun 17 '15 at 10:00
5
  1. create CommonViewController
  2. create FirstViewController (extends from CommonViewController)
  3. create SecondeViewController (extends from CommonViewController)
  4. add function common functions in the CommonViewController

like that

CommonViewController.h

@interface CommonViewController : UIViewController

-(void) initializeCartBarButton;

@end

CommonViewController.m

#import "CommonViewController.h"

@interface CommonViewController ()

@end

@implementation CommonViewController

-(void) initializeCartBarButton {


    UIBarButtonItem *cartBarButton = [[UIBarButtonItem alloc] init];
    cartBarButton.title = @"cart";
    [cartBarButton setTarget: self];
    [cartBarButton setAction: @selector(goToCart:)];

    self.navigationItem.rightBarButtonItem = cartBarButton;
    }

- (IBAction) goToCart:(id)sender {
    NSLog(@"");
  }

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
  }

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
  }

@end

FirstViewController.h

#import <UIKit/UIKit.h>
#import "CommonViewController.h"

@interface FirstViewController : CommonViewController

@end

FirstViewController.m

#import "FirstViewController.h"

@interface FirstViewController ()

@end

@implementation FirstViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self initializeCartBarButton];
}
@end

SecondViewController.h

#import <UIKit/UIKit.h>
#import "CommonViewController.h"

@interface SecondViewController : CommonViewController

@end

SecondViewController.m

#import "SecondViewController.h"

@interface SecondViewController ()

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self initializeCartBarButton];
}
@end

note: you can add the code of initializeCartBarButton in the viewDidLoad of CommonViewController and delete this fuction from CommonViewController and from child class's

Mohamad Chami
  • 1,226
  • 14
  • 10
  • @ mohamad i tried your code working fine,but it i can't able to click that bar button. – Dharma Jan 08 '16 at 10:48
  • sorry for my answer above. please try this code add this 2 line in initializeCartBarButton function after: cartBarButton.title = @"cart"; [barButton setTarget: self]; [barButton setAction: @selector(goToCart:)]; than add the function goToCart -(IBAction)goToCart:(id)sender { NSLog(@""); } – Mohamad Chami Jan 08 '16 at 12:40
  • welcom welcom, any time – Mohamad Chami Jan 08 '16 at 12:52
5

This is how I did it with UINavigationController subclass that is capable of dismissing every viewController pushed into it.

class CustomNavigationController: UINavigationController, UINavigationControllerDelegate{

    //TODO: Use when we have more right bar button types.
    var rightBarButtonType: RightBarButtonType = .Close

    enum RightBarButtonType{
        case Close
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }

    // MARK: Private Functions
    private func addRightBarButtonTo(viewController: UIViewController){

        let barButtonItem: UIBarButtonItem!
        switch self.rightBarButtonType {
        case .Close:
            barButtonItem = UIBarButtonItem(image: UIImage(named: "ic_close_white"), style: .Done, target: self, action: #selector(CustomNavigationController.dismiss(_:)))

        }
        viewController.navigationItem.rightBarButtonItem = barButtonItem
    }

    // MARK: UINavigationController Delegate
    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
        self.addRightBarButtonTo(viewController)
    }

    @objc func dismiss(sender: AnyObject){
        self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
    }
}
MadNik
  • 7,713
  • 2
  • 37
  • 39
1

You'll need to add the button in every view controller. You cannot do it by setting one once or sharing one between view controllers (in a sensible fashion). A good place to add the button is in the viewDidLoad method of your view controllers. You can create one basic UIViewConteoller subclass for them if you feel this gets to repetitive.

Johan Kool
  • 15,637
  • 8
  • 64
  • 81
0

Add this code in your rootview viewDidLoad method and implement the cancelMethod in rootview controller.This will be available in all the view controllers. you can adjust the button location by changing button frame.For orientation change you have manually adjust the location of button.

 UIButton *btnCancel = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [btnCancel addTarget:self
                        action:@selector(cancelMethod)
              forControlEvents:UIControlEventTouchDown];
        [btnCancel setBackgroundImage:[UIImage imageNamed:@"image"]  
        forState:UIControlStateNormal];
        btnCancel.frame = CGRectMake(280, 27, 45, 25);

    [self.navigationController.view addSubview: btnCancel];
Roshan
  • 71
  • 5
0

You can add a custom 'Cancel' UIButton directly to the NavigationBar's view instead of using the UIBarButtonItem.

UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
cancelButton.imageView = // Some custom image
cancelButton.frame = CGRectMake(...);  // Something far to the right.
[self.navigationController.navigationBar addSubview: cancelButton];

The normal way to do this is to add that cancel button to the navigationItem of every single view controller in your navigation stack. The above approach can make it simpler by allowing you to write less code, but it is a tiny bit of a hack.

hundreth
  • 841
  • 4
  • 8
  • Problems: (1) The button needs to send its action to some view controller. You have to work out what VC 1 should do when there are 2 more VCs above it in the stack, and any of them might be in an indeterminate state. (2) The navigation bar will be shared with the VC that pushed the VC that's modifying the navigation bar in this way, which can lead to the Cancel button appearing in places where it's inappropriate. (3) Might not play well with RTL languages. I can't read any, so I haven't played around with how the UI reconfigures itself for them. – Jeremy W. Sherman Jun 17 '11 at 17:14
  • Valid concerns, as I said it is somewhat of a hack. He really should have each individual view controller add it's own Cancel UIBarButtonItem and respond appropriately. I don't think many of your concerns apply to him though. He most likely has this wizard in some modal view controller that simply needs to dismiss when Cancel is tapped. This would work regardless of state. – hundreth Jun 17 '11 at 17:20
  • You're right, the wizard is in a modal view controller and needs to be dismissed. I thought there might be a quick way of doing this. I think I will keep it simple, use inheritance to add the button to each VC. – Ozay Jun 18 '11 at 08:07