35

I have a UINavigationController into which I push several views. Inside viewDidLoad for one of these views I want to set the self.navigationItem.backBarButtonItem to a custom view (based on a custom image). I don't know why, but it doesn't seem to work. Instead, I get the standard "back" button.

UIButton *backButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 63, 30)];
[backButton setImage:[UIImage imageNamed:@"back_OFF.png"] forState:UIControlStateNormal];
[backButton setImage:[UIImage imageNamed:@"back_ON.png"] forState:UIControlStateSelected];
UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
self.navigationItem.backBarButtonItem = backButtonItem;
[backButtonItem release];
[backButton release];

I tested with a standard title and it worked. What is wrong with the above code ?

self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Prout" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];

Thanks for any help on this.

Chris Hanson
  • 54,380
  • 8
  • 73
  • 102
François P.
  • 5,166
  • 4
  • 32
  • 31
  • did you get any compile warnings? – hhafez Feb 08 '09 at 22:47
  • I'm fairly certain that I read yesterday that the backBarButtonItem is a read-only property. No idea if there is a workaround. – Travis Feb 09 '09 at 19:49
  • Set the backBarButtonItem **before** pushing the view with the navigation controller (see answer below). – SundayMonday Aug 25 '11 at 02:11
  • Any more info on this? Still seems to be broken as of iOS 5.0.1... – blabus Nov 23 '11 at 04:08
  • "When configuring your bar button item, do not assign a custom view to it; the navigation item ignores custom views in the back bar button anyway." – typeoneerror Dec 19 '11 at 20:26
  • Your problem can be solved here (here I made a solution): [Solution link](http://stackoverflow.com/questions/8181781/back-navigationitem-with-title-background-image/13863176#13863176 "How to Add Background to BackbuttonItem") – pedrouan Dec 13 '12 at 15:38

14 Answers14

27

As of iOS5 we have an excellent new way of customizing the appearance of almost any control using the UIAppearance protocol, i.e. [UIBarButtonItem appearance]. The appearance proxy allows you to create application wide changes to the look of controls. Below is an example of a custom back button created with the appearance proxy.

enter image description here

Use the example code below to create a back button with custom images for normal and highlighted states. Call the following method from you appDelegate's application:didFinishLaunchingWithOptions:

- (void) customizeAppearance {

UIImage *i1 = [[UIImage imageNamed:@"custom_backButton_30px"]
                      resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 6)];
UIImage *i2 = [[UIImage imageNamed:@"custom_backButton_24px"] 
                      resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 6)];

[[UIBarButtonItem appearance] setBackButtonBackgroundImage:i1 
                              forState:UIControlStateNormal 
                              barMetrics:UIBarMetricsDefault];

[[UIBarButtonItem appearance] setBackButtonBackgroundImage:i2 
                              forState:UIControlStateNormal 
                              barMetrics:UIBarMetricsLandscapePhone];

[[UIBarButtonItem appearance] setBackButtonBackgroundImage:i1
                              forState:UIControlStateHighlighted 
                              barMetrics:UIBarMetricsDefault];

[[UIBarButtonItem appearance] setBackButtonBackgroundImage:i2 
                              forState:UIControlStateHighlighted 
                              barMetrics:UIBarMetricsLandscapePhone];
}

This is just a quick example. Normally you would want to have separate images for normal and highlighted (pressed) state.

If you are interested in customizing the appearance of other controls, some good examples can be found here: http://ios.biomsoft.com/2011/10/13/user-interface-customization-in-ios-5/

memmons
  • 40,222
  • 21
  • 149
  • 183
  • Really wish I had of found this answer quicker than I did... Works perfectly, thanks! – Jarrod Nov 01 '12 at 01:27
  • 3
    Does not work if you need to use custom views. i.e. if you need to make the back button a different size or shape to default. Is the easiest if you don't need this though. – Imran Jan 16 '13 at 08:50
  • @imran You have a good point. Typically though, you'll almost always want to stick to standard sizes for custom controls. Not always, of course. – memmons Apr 07 '13 at 14:11
  • @Answerbot usually I try to. Designer came up with some xtra large buttons once though and I didn't want to ruin the look of the app ;). – Imran Apr 08 '13 at 10:23
19

I'm fairly certain that the backBarButtonItem is a read-only property. Instead of modifying the backBarButtonItem, try setting a custom leftBarButtonItem and hide the backBarButtonItem:

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Prout" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;

You will also need to make sure you hook up the custom button to call the back action on the UINavigationBar.

Travis
  • 576
  • 5
  • 13
  • 30
    backBarButtonItem is *not* a read-only property. I'm not sure why it behaves so strangely, and the above is a valid (if less-than-ideal) workaround. – Adam Ernst Mar 10 '09 at 14:37
  • 3
    As Adam said, `backBarButtonItem` is **not** read-only. This is not an ideal workaround, but definitely acceptable. – Sam Soffes Aug 12 '10 at 18:00
  • 3
    I think the backBarButtonItem property gives the label for the back button in any *child* view controllers; that is, the label for the button that pops back up to the given view controller. – David M. Sep 13 '10 at 19:44
  • 9
    The `leftBarButtonItem` does not animate like a back button. It will fade without moving horizontally when view controllers are pushed and popped. – lemnar Apr 07 '11 at 12:34
  • Looks like this is an iOS bug... please be awesome and dupe it, like i did :) http://openradar.appspot.com/9160693 – Sahil Jun 28 '11 at 00:27
  • the back bar button item is used when there is a view controller on top of this view controller, to go back to this view controller. To replace this with the left bar button item, you would have to set this in the child view controller every time you push one. – user102008 Dec 16 '11 at 00:57
  • 15
    It's not a bug. From the docs -> "When configuring your bar button item, do not assign a custom view to it; the navigation item ignores custom views in the back bar button anyway." – typeoneerror Dec 19 '11 at 20:26
  • 3
    The entire phrase from the docs is, **If you want to specify a custom image or title for the back button, you can assign a custom bar button item (with your custom title or image) to this property instead. When configuring your bar button item, do not assign a custom view to it; the navigation item ignores custom views in the back bar button anyway.** So, you can use a custom image on the barButtonItem, you just can't use a custom view. – memmons Mar 21 '12 at 20:28
  • To fix the animation: [UIView animateWithDuration:0.5 animations:^{ self.navigationItem.leftBarButtonItem.customView.frame = CGRectMake(320, frame.origin.y, frame.size.width, frame.size.height); }]; – Imran Jan 16 '13 at 08:48
  • @Imran Why hack a fix though when you can customize it using UIAppearance protocol? – memmons Mar 14 '13 at 20:54
  • @Answerbot because you cant do everything with UIAppearance for example if you want to change the frame height, its impossible unless you use your own custom button, in which case adding the animation is a nice finishing touch. – Imran Mar 14 '13 at 21:00
  • @Answerbot your answer was still valid, just thought the animation code would be helpful for people that need to stuff outside of UIAppearance as I had this requirement once. – Imran Mar 14 '13 at 21:31
15

I never been able to create a proper UIBarButtonItem with custom view and setBackBarButtonItem.

Here's the solution i found : let net UINavigationControllerDelegate handles everything! The trick here is to call the popViewControllerAnimated: method of the viewController.navigationController so you don't have to create any custom method.

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if([navigationController.viewControllers count ] > 1) {
        UIView *backButtonView = [[UIView alloc] initWithFrame:CGRectMake(0,0,70,35)];
        UIButton *myBackButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
        [myBackButton setFrame:CGRectMake(0,0,70,35)];
        [myBackButton setImage:[UIImage imageNamed:@"back.png"] forState:UIControlStateNormal];
        [myBackButton setEnabled:YES];
        [myBackButton addTarget:viewController.navigationController action:@selector(popViewControllerAnimated:) forControlEvents:UIControlEventTouchUpInside];
        [backButtonView addSubview:myBackButton];
        [myBackButton release];
        UIBarButtonItem* backButton = [[UIBarButtonItem alloc] initWithCustomView:backButtonView];
        viewController.navigationItem.leftBarButtonItem = backButton;
        [backButtonView release];
        [backButton release];
    }
}
Vivi
  • 972
  • 1
  • 14
  • 30
  • 4
    This solution is correct however the addTarget: call is not. In this case you are setting the button action to popViewControllerAnimated: which because of the target/action mechanism will be passed the UIButton that is clicked on. popViewControllerAnimated: takes a BOOL value. So whether or not the transition is animated will be a side-effect of the location in memory where the button is allocated and how Apple implements the popViewControllerAnimated: call (e.g. do they do "if (animated)" or "if (animated == YES)". Instead, set a method that calls popToViewControllerAnimated:YES. – par Oct 22 '10 at 09:37
  • 2
    This solution (taking par's advice into account) is a more appropriate answer to the original question than the currently accepted answer, I feel. It does allow you to actually use a custom view. – Jonathan Zhan Mar 03 '11 at 12:00
  • Please don't do this as of iOS 5 -- fragile and hacky. – memmons Mar 21 '12 at 21:45
  • You should create a separate CGRect structure because you write the same frame in your code twice. The second mistake is using setImage instead of setBackgroundImage – Gargo Aug 30 '12 at 08:33
  • this work for me. Taking the advice of par, You may call your own method witch will receive the sender and from there call popToRootViewControllerAnimated:<(BOOL)>. (correct me, if I'm wrong) – Frade Apr 05 '13 at 11:07
  • This is a tidy solution! Additionally, I implemented willAnimateRotationToInterfaceOrientation to adjust the customView frame height on orientation change so the buttons resize nicely and also each UIBarButtonItem in topViewController.toolbarItems so my custom toolbar icons resize as well – Rafael Nobre Sep 24 '13 at 21:10
10

If your end goal is to simply replace the image used for the back button, you can use a new method on UIBarButtonItem available in iOS 5.0:

setBackButtonBackgroundImage:forState:barMetrics:

Apple Docs: http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIBarButtonItem_Class/Reference/Reference.html

Here's a simple example that sets a custom background image for all back buttons in your app:

UIImage *toolbarBackButtonBackgroundPortrait = [[UIImage imageNamed:@"toolbarBackButtonPortrait"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 17, 0, 6)];
UIImage *toolbarBackButtonBackgroundLandscape = [[UIImage imageNamed:@"toolbarBackButtonLandscape"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 17, 0, 6)];

[[UIBarButtonItem appearance] setBackButtonBackgroundImage:toolbarBackButtonBackgroundPortrait forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:toolbarBackButtonBackgroundLandscape forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
Paul
  • 183
  • 1
  • 7
  • 3
    Oh my god THANK YOU. This has been driving me crazy (because I've seen apps that implement it successfully), and your solution works perfectly. FYI for anyone else- there is a big difference between setting a custom LEFT button and BACK button (mainly that the back button gets a smooth panning animation when pushing/popping view controllers, where as the left button just fades in/out). – blabus Dec 10 '11 at 21:25
  • 1
    how do we remove the title from the back button. I can set the image but the title overlaps it. – ArdenDev Apr 04 '13 at 12:22
6

I would be willing to bet that this is a bug on Apple's part as I am running into the exact same problem. The reason being is that I can get a custom UIBarButtonItem to appear, but only when I don't try to use the initWithCustomView: method. Per the API, the navigation controller checks the following things when a new view controller is pushed:

  1. If the new top-level view controller has a custom left bar button item, that item is displayed. To specify a custom left bar button item, set the leftBarButtonItem property of the view controller’s navigation item.
  2. If the top-level view controller does not have a custom left bar button item, but the navigation item of the previous view controller has a valid item in its backBarButtonItem property, the navigation bar displays that item.
  3. If a custom bar button item is not specified by either of the view controllers, a default back button is used and its title is set to the value of the title property of the previous view controller—that is, the view controller one level down on the stack. (If there is only one view controller on the navigation stack, no back button is displayed.)

My case (as well as yours) is 2. I specify code exactly the same as yours (i.e., creating a UIButton, setting its image properties for various states, creating a UIBarButtonItem, initializing it with the UIButton, then setting my current view controller's backBarButtonItem property to the UIBarButtonItem); however, when I later push my view controller, nothing at all is displayed on the left-hand side of my navigation controller. Strangely, I can click where the "Back" button should be, and it pops the view controller.

Interestingly, if I create a UIBarButtonItem using the initWithTitle:style:target:action: method instead of the initWithCustomView: method, it does show a custom button with a custom title. Also, as Travis mentioned, using the leftBarButtonItem property instead works just fine. I'd rather adhere to the sanctioned logic, however, by specifying the "Back" button for the current view controller -- to be displayed later when a new view controller is pushed -- instead of creating a left button for the next view controller, which, arguably, should have no concern for anything pertaining to the view controller that came before it. :-\

CIFilter
  • 8,647
  • 4
  • 46
  • 66
  • I am experiencing this issue as well. It works unless you use `initWithCustomView:` which is quite frustrating. – Sam Soffes Aug 12 '10 at 18:02
5

Set the backBarButtonItem before pushing the viewController with the navigationController. Setting the backBarButtonItem in viewDidLoad doesn't work.

Say I have MyTableViewController. When the user taps a particular row I want to push AnotherViewController using the navigationController. The code looks something like this:

// in MyTableViewController's tableView:didSelectRowAtIndexPath method...
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] 
                               initWithImage:[UIImage imageNamed:@"yourImage.png"]
                                       style:UIBarButtonItemStyleBordered 
                                      target:nil 
                                      action:nil];

self.navigationItem.backBarButtonItem = backButton;
[backButton release];

[self.navigationController pushViewController:anotherViewController];

When anotherViewController is displayed the back button in the navigation bar will have @"yourImage.png" and the default back button style (rectangular arrow). Also note it's fine to pass nil as target. The button behaves like the back button.

SundayMonday
  • 19,147
  • 29
  • 100
  • 154
  • 4
    You are creating a `UIBarButtonItem` with an image, not with a custom view. Not the same thing. – ySgPjx Oct 21 '11 at 19:06
2

Even though is already answered this worked for me:

UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"backArrow.png"] style:UIBarButtonItemStyleBordered target:nil action:nil];
self.navigationItem.backBarButtonItem = backButton;
[backButton release];

BTW: even in iOS4 initializing my back button with initWithCustomView: didn't work for me. ;(

nacho4d
  • 43,720
  • 45
  • 157
  • 240
2

I too have been having problems with customView on a navigationItem.backBarButtonItem. I suspect it's probably just b0rked in the SDK.

While the workarounds outlined here do work, I've come up with a solution which is a little more elegant: it still uses navigationItem.leftBarButtonItem, but takes care of it for you automagically (no more need for 'child' view controllers to need to know anything about their 'parent' and/or to manually set navigationItem.leftBarButtonItem).


First up, have some class be a UINavigationControllerDelegate for the UINavigationController whose back button you're interested in. Then, in this class, set up something like the following willShowViewController delegate method:

-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
    // reference to view controller stack
    NSArray *viewControllers = [ navigationController viewControllers ];
    if( [ viewControllers count ] > 1 ){
        // the view controller we'll be linking to
        UIViewController *backViewController = [ viewControllers objectAtIndex: [ viewControllers count ] - 2 ];
        // create custom UIBarButtonItem
        UIBarButtonItem *leftButton = [[ UIBarButtonItem alloc ] initWithCustomView: someCustomView ];
        // set it as the leftBarButtonItem on the incoming viewcontroller
        viewController.navigationItem.leftBarButtonItem = leftButton;
        // tidy up
        [ leftButton release ];
    }
}

I had some further problems with this; it seems that UIBarButtonItem.action and UIBarButtonItem.target don't work when it's a navigationItem.leftBarButtonItem. So, you're left with a custom back button that doesn't actually go back. I'll leave responding to touches in your custom view as an exercise for the reader (I used a UIButton), but you'll need add this method to your delegate class:

-(void)onDummyBackButtonTapped{
    [ someNavigationController popViewControllerAnimated: YES ];
}

and hook it up to fire when your custom view is tapped.

Henry Cooke
  • 2,563
  • 2
  • 23
  • 22
  • Good idea. Worked for me by changing one line to UIViewController *backViewController = [ viewControllers objectAtIndex: [ viewControllers count ] - 1 ]; +1 for your answer – Shyam Bhat Dec 27 '11 at 10:08
1

This is how I create a custom square back button with an arrow instead of the usual text.

I simply setup a delegate for my UINavigationController. I use the app delegate for that because the window root view controller is the UINavigationController i want to control.

So AppDelegate.m (ARC):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ((UINavigationController*)_window.rootViewController).delegate = self;
    return YES;
}

#pragma mark - UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if(navigationController.viewControllers.count > 1) {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button setImage:[UIImage imageNamed:@"back-arrow.png"] forState:UIControlStateNormal];
        [button setBackgroundColor:[UIColor grayColor]];
        button.frame = CGRectMake(0, 0, 44, 44);
        viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
        [button addEventHandler:^(id sender) {
            [navigationController popViewControllerAnimated:YES];
        } forControlEvents:UIControlEventTouchUpInside];
    }
}

I'm using BlocksKit to catch the button tap event. It's very convenient for stuff like this but you can also use the regular addTarget:action:forControlEvents: method

drdator
  • 361
  • 3
  • 7
1

backBarButtonItem is not a read-only property. I'm not sure why it behaves so strangely, and the above is a valid (if less-than-ideal) workaround.

It behaves strangely because setting a vc's backBarButtonItem doesn't change anything about the appearance of the vc's navigation item - instead, it changes the button that points BACK to the vc. See updating the navigation bar from Apple FMI.

That said I haven't had a whole lot of luck getting it to work myself. If you look around this site, you'll find some threads that suggest placing code very similar to what you already have immediately before the call to push a new view on the stack. I've had some luck there, but unfortunately not when it comes to using a custom image.

beOn
  • 1,799
  • 1
  • 12
  • 13
1

The navigationController's backBarButtonItem is set on the item whose title you're trying to affect.

i.e. in Page 1's view controller, say, viewdidLoad:

self.title = @"Page 1 of 4";

self.navigationItem.backBarButtonItem =
[[[UIBarButtonItem alloc] initWithTitle:@"Page 1"
                                  style:UIBarButtonItemStyleBordered
                                 target:nil
                                 action:nil] autorelease];

You would not override this in Page 2.

Documentation for UINavigationItem : backBarButtonItem makes this clear:

When this item is the back item of the navigation bar—when it is the next item below the top item—it may be represented as a back button on the navigation bar. Use this property to specify the back button. The target and action of the back bar button item you set should be nil. The default value is a bar button item displaying the navigation item’s title.

0

My Logic:

  1. Create a custom button (aka a custom view subclass)
  2. Initialize a barbutton item with the custom button/view
  3. Add an action that allows us to "go back" to our previous view controller
  4. Set the left bar button item to this custom bar button item you created
  5. Hide the back bar button item of the view controller you're pushing to

Step 3 was important. I figured the cleanest way to simulate the "go back" was to just utilize UINavigationController's method (popViewControllerAnimated:). So, I just add that action to the navigationController of the viewController I'm pushing (viewControllerToPush) like so:

[navItemButton addTarget:viewControllerToPush.navigationController action:@selector(popViewControllerAnimated:) forControlEvents:UIControlEventTouchUpInside];

Example:

UIViewController *viewControllerToPush = [[UIViewController alloc] init];
UIImage *navImage = [UIImage imageNamed:@"your_back_button_image"];

UIButton *navItemButton = [UIButton buttonWithType:UIButtonTypeCustom];
[navItemButton setImage:navImage forState:UIControlStateNormal];
[navItemButton setImage:navImage forState:UIControlStateHighlighted];
[navItemButton setFrame:CGRectMake(0, 0, navImage.size.width, navImage.size.height)];

[navItemButton addTarget:viewControllerToPush.navigationController action:@selector(popViewControllerAnimated:) forControlEvents:UIControlEventTouchUpInside];

UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:navItemButton];

viewControllerToPush.navigationItem.leftBarButtonItem = barButtonItem;
viewControllerToPush.navigationItem.hidesBackButton = YES;
[self.navigationController pushViewController:viewControllerToPush animated:YES];
nmante
  • 319
  • 2
  • 10
  • These worked for me in terms of changing the picture, but viewControllerToPush.navigationItem.hidesBackButton = YES; seems to remove the ability to slide to go back, which is a dealbreaker for me. – liampronan Jan 06 '16 at 03:51
0

You can use leftBarButtonItem instead of back button item. And to remove the default back button item set it to nil like follows;

navigationController?.navigationBar.backIndicatorImage = nil
navigationController?.navigationBar.backIndicatorTransitionMaskImage = nil

let button = UIButton.init(type: .custom)
button.imageView?.contentMode = UIViewContentMode.scaleAspectFit
button.setImage(UIImage.init(named: "top_back"), for: UIControlState.normal)
button.frame = CGRect.init(x: 0, y: 0, width: 75, height: 50) 
button.addTarget(self, action: #selector(handleBackButton), for: .touchUpInside)

let barButton = UIBarButtonItem.init(customView: button)
self.navigationItem.leftBarButtonItem = barButton
Ammar Mujeeb
  • 1,222
  • 18
  • 21
0

I think I found the solution for this. Simply set the button on the navigation item on the previous controller (The one that you want to go back to)

So if I have for example a root controller and I push a second controller and want to customize the back button then I should do the following:

self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc]
    initWithImage:[UIImage imageNamed:@"ico.png"] style:UIBarButtonItemStyleBordered
    target:nil action:nil] autorelease];

Where self is the root view controller and not the second one.

sth
  • 222,467
  • 53
  • 283
  • 367
Ronen
  • 9
  • 1