May 12, 2009

Multiple Buttons on a Navigation Bar

Previously, I wrote about how to use standard icons in your navigation bar. This takes advantage of the nav bar's ability to put a button on the right side. But what if you want to put multiple buttons there? If your title is short enough, you can easily fit 2 or 3 buttons in that space. The nav bar allows you to stick content on the right hand side via the rightBarButtonItem property. On the surface, you might think you can add but one button to this property. But it's not so. There is an initializer for UIBarButtonItem that takes a view:
initWithCustomView:(UIView *)customView
With this, you can create multiple UIButton objects, add them to a UIView, and then create a UIBarButtonItem passing in this parent UIView. The following pseudo-code shows what I mean:
// create the container
UIView* container = [[UIView alloc] init];

// create a button and add it to the container
UIButton* button = [[UIButton alloc] init...];
[container addSubview:button];
[button release];

// add another button
button = [[UIButton alloc] init...];
[container addSubview:button];
[button release];

// now create a Bar button item
UIBarButtonItem* item = [[UIBarButtonItem alloc] initWithCustomView:container];

// set the nav bar's right button item
self.navigationItem.rightBarButtonItem = item;
[item release];

But, what if you want multiple *standard* buttons. Access to the standard buttons/icons is through the UIBarButtonItem class, not UIButton. You can't add UIBarButtonItem objects to a UIView. While there is support to create a UIBarButtonItem object from a UIView-derived object, there does not seem to be a way to do the opposite; there is no way to create a UIView object from a UIBarButtonItem. The only way I found how to deal with multiple UIBarButtonItem objects is to stick them in a toolbar. A UIToolBar is designed to hold multiple UIBarButtonItem objects. Think of a toolbar as your container, instead of the UIView object. And since a UIToolBar is UIView based, it can be added to the right side of a nav bar using the above trick. The following code shows how you can add two standard buttons to the right side:

// create a toolbar to have two buttons in the right
UIToolbar* tools = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 133, 44.01)];

// create the array to hold the buttons, which then gets added to the toolbar
NSMutableArray* buttons = [[NSMutableArray alloc] initWithCapacity:3];

// create a standard "add" button
UIBarButtonItem* bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:NULL];
bi.style = UIBarButtonItemStyleBordered;
[buttons addObject:bi];
[bi release];

// create a spacer
bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
[buttons addObject:bi];
[bi release];

// create a standard "refresh" button
bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refresh:)];
bi.style = UIBarButtonItemStyleBordered;
[buttons addObject:bi];
[bi release];

// stick the buttons in the toolbar
[tools setItems:buttons animated:NO];

[buttons release];

// and put the toolbar in the nav bar
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:tools];
[tools release];


37 comments:

  1. Thank you, very (very) helpful indeed.

    ReplyDelete
  2. Thanks, that was useful. When I use the 2nd recipe, I see both buttons appear how i want them, but the toolbar is just not a perfect fit on the navigation bar... at 45 pixels, it seems a pixel too much as it extends below into the tableview, and at 44 or less, it shows a line at the top, meaning it's too small. Did you experience the same problem, or does yours fit perfectly?

    ReplyDelete
  3. Same problem here , I see some underline

    ReplyDelete
  4. I also see this, but thought maybe it was a problem with the simulator. Has anyone seen this problem while running on an actual iPhone device?

    ReplyDelete
  5. Seem to be helpful, but can you write down the whole code for the first case (without "..."). Sorry, but I am an absolute beginner...

    ReplyDelete
  6. Excellent! Very clean, and I just dropped it in my code and it ran -- thanks.

    ReplyDelete
  7. JES, yes I see the nav bar line artifacts on my iPhone running 3.0.1 built on 3.0 SDK. I worked around it by setting the toolbar height to 44.01. That avoids the top line artifact while not stomping on the line below. For my own two standard buttons and spacer, I used:
    UIToolbar* tools = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, 0.0, 133.0, 44.01)];

    ReplyDelete
  8. Hi Paul, thanks for verifying this on an actual device. I will update the code in the post.

    ReplyDelete
  9. but how to give the buttons a style, cos wen i select the button i don get a effect of it being selected??

    ReplyDelete
  10. There seems to be a misalignment of the UIToolbar with respect to the navigation bar visible slightly in the shading.

    If you use 44.01 as the height, the bottom lines up, but you get a brighter white line at the top and a misalignment of the shadow in the middle. other values, 45, 44, 43.4, 43.6 all have worse problems.

    Is there a better solution or work around?

    ReplyDelete
  11. In a word, EXCELLENT!

    ReplyDelete
  12. I have solution for this.

    If you menage to do toolBar background transparent, just buttons visible, then you dont have to look at exact height.
    Setting backgroundColor property is not enough, so i found this solution: create new class which inherit from UIToolbar and implement drawing method - (void)drawRect:(CGRect)rect .
    Let this method blank.. now toolBar will be transparent, but buttons, etc.. added by setItems method will be normaly visible.

    ReplyDelete
  13. I'd also like to thank Petr Fiala - spent hours trying to get transparency on a ToolBar and implemement his solution - worked immediately.

    Brilliant.

    ReplyDelete
  14. Watch out for possible leak in this part of the above code:
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:tools];

    You could use this instead:

    UIBarButtonItem* rightButtonBar = [[UIBarButtonItem alloc] initWithCustomView:tools];
    self.navigationItem.rightBarButtonItem = rightButtonBar;
    [rightButtonBar release];
    ;) ... or use autorelease

    ReplyDelete
  15. Petr Fiala

    Your solution sound brilliant. But I am not getting it right.
    Can you please elaborate it. You will make my day.

    Tanks
    MrT

    ReplyDelete
  16. I haven't tried this on the iPhone, but I've used it on the iPad (the toolbar idea). Unfortunately, the rightmost button (if this technique is used on the right side) is too far away horizontally from the edge of the navigation bar. Is there a way to fix this?

    ReplyDelete
  17. Yea,
    In the code that creates the UIToolbar:

    [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 133, 44.01)];

    Set the width (133) to something less.

    Sam

    ReplyDelete
  18. Great, very helpful!!!

    ReplyDelete
  19. Helped me out! - i was stuck exactly with this problem, and on an iPad it makes sense to have more than one 'right' item.

    ReplyDelete
  20. Petr Fiala's solution can be extended to the other styles as long as you also include:

    [toolbar setBackgroundColor:[UIColor clearColor]];

    ReplyDelete
  21. While you're at it, I'd also recommend overwriting another method in your custom UIToolbar class:
    - (void)setFrame:(CGRect)rect {}

    This will allow your toolbar to resize when rotated and it will also give you the opportunity to elevate the toolbar by 1 pixel so it directly lines up with the rest of the navigation bar. Below is what I use... depending on how many items you use and how wide they are will depend on your x offset. (I'm using a + button and an edit button.)

    - (void)setFrame:(CGRect)rect {

    if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortrait) {
    [super setFrame:CGRectMake(216, -1, rect.size.width, 44)];
    }
    else {
    [super setFrame:CGRectMake(381, -1, rect.size.width, 32)];
    }

    }

    ReplyDelete
  22. you are the best

    ReplyDelete
  23. thx guys, useful info.. in my case i am using tint colors, and buttons were always appearing in blue, even when overriding drawRect, and setting bgColor to clear..

    my solution was to find the subviews (buttons) of the toolbar and set color on those:

    for (UIView *view in [self subviews]) {
    [view setTintColor: [self tintColor]];
    }

    ReplyDelete
  24. This comment has been removed by the author.

    ReplyDelete
  25. The custom icons doesn´t work in IOs4.
    This is what i´m doing


    // create the container

    UIView* container = [[UIView alloc] init];

    // create a button and add it to the container
    UIButton* button = [[UIButton alloc] init];

    UIImage *img = [UIImage imageNamed:@"close.png"];
    [button setImage:img forState:UIControlStateNormal];
    [img release];
    [container addSubview:button];
    [button release];

    // add another button
    button = [[UIButton alloc] init];
    img = [UIImage imageNamed:@"close.png"];
    [button setImage:img forState:UIControlStateNormal];
    [img release];
    [container addSubview:button];
    [button release];


    // now create a Bar button item
    UIBarButtonItem* item = [[UIBarButtonItem alloc] initWithCustomView:container];

    // set the nav bar's right button item
    self.navigationItem.rightBarButtonItem = item;
    [item release];

    ReplyDelete
  26. You are releasing UIImage objects that were not alloc'd. Remove the two [img release]; statements and see if that helps.

    ReplyDelete
  27. I found it was easiest to do this trick using a regular UIView and regular UIButtons inside it (setting the UIView as the argument for initWithCustomView)

    To make it look right I took a snapshot of the Nav Bar background and used it as my UIView background, and the UIButtons are regular buttons but I used snapshots of a standard button's background when normal and when highlighted as the basis for the buttons. The end result is that it looks perfect while not using any strange coding tricks like setting a height to 44.01.

    Downside is , if Apple ever decides to change the style of the navigation bar and system buttons, I'll have to update my app with the new graphics.

    ReplyDelete
  28. Thanks, this helped me a lot, I made some changes to it and documented the code here: http://justabloglol.blogspot.com/2011/06/commentpublic-class-testing-public.html

    ReplyDelete
  29. Is there any way of alignment of UIBarButtonItem, We are showing two button add and delete, if there is no items then we are not showing delete button and 'Add' button should be in place of 'Delete' but once 'Delete' button appears 'Add' button should move to left to give the space for 'Delete'. there is only one trick either i need to add one more empty custom button to fill the space and move the 'Add; button to right most but i want other better solution if is there any.

    ReplyDelete
  30. FYI, iOS 5.0 now supports multiple buttons. See the iOS documentation for UINavigationItem. Specifically, the following:

    Properties:
    @property(nonatomic, copy) NSArray *leftBarButtonItems
    @property(nonatomic, copy) NSArray *rightBarButtonItems
    @property BOOL leftItemsSupplementBackButton


    Methods:
    - (void)setLeftBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated;
    - (void)setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated;

    ReplyDelete
  31. Thnks a lot!! very helpful!

    ReplyDelete
  32. Thank you, V much, But the targets are calling only for one right Barbutton? I cant get the call another methods

    ReplyDelete
  33. awesome man, works like a charm!

    ReplyDelete
  34. Update: You can now do the following to add two buttons on the left:

    UIBarButtonItem *refreshBtn = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refreshPlans:)];
    UIBarButtonItem *selectYearBtn = [[UIBarButtonItem alloc] initWithTitle:@"Select Year" style:UIBarButtonSystemItemAction target:self action:@selector(selectYear)];
    self.navigationItem.leftBarButtonItems = [[NSArray alloc] initWithObjects: refreshBtn, selectYearBtn, nil];

    Unfortunately, I haven't seen a way to do this in the Storyboard. Hope this helps.

    ReplyDelete
  35. Can therse be added to the titleView? Or centre of the navigation bar? Similar to Facebook iPhone centre buttons on nav bar? Thanks!

    ReplyDelete
  36. I always tried using "rightBarButtonItems" and just had problems. Seems using a UIToolbar works. Thanks!

    ReplyDelete