50

I've been trying to set up a custom background for the whole of my NavigationBar (not just the titleView) but have been struggling.

I found this thread

http://discussions.apple.com/thread.jspa?threadID=1649012&tstart=0

But am not sure how to implement the code snippet that is given. Is the code implemented as a new class? Also where do I instatiate the UINavigationController as I have an application built with the NavigationView template so it is not done in my root controller as per the example

dymv
  • 3,252
  • 2
  • 19
  • 29
Anthony Main
  • 6,039
  • 12
  • 64
  • 89

15 Answers15

44

Uddhav and leflaw are right. This code works nicely:

@interface CustomNavigationBar : UINavigationBar
@end

@implementation CustomNavigationBar
-(void) drawRect:(CGRect)rect 
{
    UIImage *image = [UIImage imageNamed: @"myNavBarImage"];
    [image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
}
@end

// this can go anywhere
+(UINavigationController*) myCustomNavigationController
{
  MyViewController *vc = [[[MyViewController alloc] init] autorelease];
  UINavigationController *nav = [[[NSBundle mainBundle] loadNibNamed:@"CustomNavigationController" owner:self options:nil] objectAtIndex:0];
  nav.viewControllers = [NSArray arrayWithObject:vc];
  return nav;
}

You have to create CustomNavigationController.xib and put a UINavigationController in it and change the navigationBar class to "CustomNavigationBar".

EricS
  • 9,650
  • 2
  • 38
  • 34
31

You must use the 'appearance' proxy to change the background and other styling properties of controls such as UINavigationBar, UIToolBar etc. in iOS 5.xx. However, these are not available for iOS 4.xx so for backwards compatibility, you need a hybrid solution.

If you want to support both iOS 4.xx and iOS 5.xx devices (i.e. your DeploymentTarget is 4.xx), you must be careful in wrapping the call to the appearance proxy by checking at runtime if the 'appearance' selector is present or not.

You can do so by:

//Customize the look of the UINavBar for iOS5 devices
if ([[UINavigationBar class]respondsToSelector:@selector(appearance)]) {
    [[UINavigationBar appearance] setBackgroundImage:[UIImage imageNamed:@"NavigationBar.png"] forBarMetrics:UIBarMetricsDefault];
}

You should also leave the iOS 4.xx workaround that you may have implemented. If you have implemented the drawRect workaround for iOS 4.xx devices, as mentioned by @ludwigschubert, you should leave that in:

@implementation UINavigationBar (BackgroundImage)
//This overridden implementation will patch up the NavBar with a custom Image instead of the title
- (void)drawRect:(CGRect)rect {
     UIImage *image = [UIImage imageNamed: @"NavigationBar.png"];
     [image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
}
@end

This will get the NavBar look the same in both iOS 4 and iOS 5 devices.

pablasso
  • 2,479
  • 2
  • 26
  • 32
bhavinb
  • 3,278
  • 2
  • 28
  • 26
29

You just have to overload drawRect like that :

@implementation UINavigationBar (CustomImage)
- (void)drawRect:(CGRect)rect {
    UIImage *image = [UIImage imageNamed: @"NavigationBar.png"];
    [image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
}
@end
Lithium
  • 1,132
  • 11
  • 10
  • 1
    Is this the 'recommended' resp. 'valid' way to override this with a category? –  Nov 15 '10 at 17:02
  • How would one assign the image from the ViewController? Essentially, how do I extend - (void)drawRect:(CGRect)rec with - (void)drawRect:(CGRect)rec withImageName:(NSString *)imageName – Bryan Jan 17 '11 at 18:49
  • 3
    This worked for me very well until I used the MPMoviePlayerViewController. Apparently they use a subclassed UINavigationBar so my custom bar was showing up on that. I had to use @ardalahmet's [method below](http://stackoverflow.com/questions/704558/custom-uinavigationbar-background/4983492#4983492), but added a call to `super` if its not a `UINavigationBar` using `isMemberOfClass:[UINavigationBar class]` – Jon Mar 11 '11 at 20:02
  • 5
    Overriding methods using a category is not a good idea. At runtime, it is undefined which version of the implementation will be used. – Jasarien Apr 04 '11 at 13:18
  • The problem you have is that if you use a navigation controller, the title of each page will overlay your custom nav bar. You could set the title to blank, but some views force a title (like the photo picker). To switch nav bars at will, add a property to your app delegate to hold the name of the nav-bar image and replace the first line above with these two: – Oscar Apr 14 '11 at 07:22
  • 38
    Apple specifically recommend against this technique. You should subclass UINavigationBar and overload drawRect. Then in your XIB specify your subclass as the class for the UINavigationBar. – orj Aug 21 '11 at 11:54
  • 17
    Looks like this method is broken with iOS 5. – Ivan Vučica Nov 16 '11 at 13:06
  • That's because in iOS 5 there are new public API methods for doing this: you need to conditionally code so this only executes on iOS 4 and below – lxt Apr 24 '12 at 10:54
  • You should use the [appearance proxy](http://developer.apple.com/library/ios/#documentation/uikit/reference/UINavigationBar_Class/Reference/UINavigationBar.html#//apple_ref/doc/uid/TP40006887-CH3-SW23) to do this in iOS 5.0 and later, and subclasses to do it in previous versions. Overriding methods on a category [is strongly discouraged.](http://stackoverflow.com/a/5272612/798224) – Phil Calvin Aug 27 '12 at 20:43
  • since this code you cannot setup the title of your navigation bar – Gargo Sep 03 '12 at 08:01
  • @IvanVučica it's not broken but you have to make a subclass and have to use in your custom navigation controller. UINavigationBar category won't work with iOS 5 – Alex Markman Jun 06 '13 at 23:07
  • @AlexMarkman Exactly what I meant. Plus, using appearance proxy, as Phil Calvin suggested, is the right way to go for 99% of customizations. – Ivan Vučica Jun 08 '13 at 19:46
26

Implementing a category is not advisable. iOS5 may provide relief for this issue. But for old APIs, you can -

  1. Subclass UINavigationBar to say CustomNavBar and implement the custom drawRect from Lithium's answer.
  2. For all IB based UINavigationControllers, provide CustomNavBar as custom class for their UINavigationBar.
  3. For all code based UINavigationControllers. Create a XIB with a UINavigationController and do step two. Then provide a factory method in code that loads the UINavigationController from the nib and provide an IBOutlet.

Eg.

[[NSBundle mainBundle] loadNibNamed:@"CustomNavigationController" owner:self options:nil];
UINavigationController *navController = self.customNavigationController;
navController.viewControllers = [NSArray arrayWithObject:controller]
pablasso
  • 2,479
  • 2
  • 26
  • 32
Uddhav Kambli
  • 448
  • 6
  • 11
  • Excellent, this approach is working for me on iOS 4.2.1 and iOS 5.0.1 – raidfive Nov 29 '11 at 19:32
  • Good answer - please note this also works for getting a view reference out of a xib (no outlets needed) `UINavigationController * navigationController = [[[NSBundle mainBundle] loadNibNamed:@"CustomNaviagionController" owner:nil options:nil] lastObject];` – Robert Jun 02 '13 at 22:02
10

You can also override the drawLayer:inContext: method in a UINavigationBar category class. Inside the drawLayer:inContext: method, you can draw the background image you want to use.

- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    if ([self isMemberOfClass:[UINavigationBar class]] == NO) {
        return;
    }

    UIImage *image = (self.frame.size.width > 320) ?
                        [UINavigationBar bgImageLandscape] : [UINavigationBar bgImagePortrait];
    CGContextClip(context);
    CGContextTranslateCTM(context, 0, image.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextDrawImage(context, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height), image.CGImage);
}

And as a complete demo Xcode project on customizing the appearance of UINavigationBar this and this might be helpful.

pablasso
  • 2,479
  • 2
  • 26
  • 32
Ahmet Ardal
  • 1,162
  • 1
  • 11
  • 12
  • 1
    Do not do this. Subclass UINavigationBar and implement drawRect:. Specify your subclass as the class for the UINavigationBar in your XIBs. – orj Aug 21 '11 at 11:56
4

I just found this blog entry, describing this topic very simple: http://web0.at/blog/?p=38

it helped me a lot, they use the "drawRect" method to get the customisation of the background.

Darren
  • 68,902
  • 24
  • 138
  • 144
d4rwin
  • 41
  • 1
4

Implementing a category won't work in iOS5, you should use Uddhav Kambli's advice for using CustomNavbar on iOS ≤ 5.

dymv
  • 3,252
  • 2
  • 19
  • 29
  • 5
    In the iOS 5 beta, the UINavigationBar, UIToolbar, and UITabBar implementations have changed so that the drawRect: method is not called on instances of these classes unless it is implemented in a subclass. Apps that have re-implemented drawRect: in a category on any of these classes will find that the drawRect: method isn't called. UIKit does link-checking to keep the method from being called in apps linked before iOS 5 but does not support this design on iOS 5 or later. – dymv Aug 15 '11 at 16:31
  • Thanks for that reply. Do you think this is to encourage people to sub class instead of doing the drawRect globally on all instances using a category? – Camsoft Aug 15 '11 at 17:18
  • 2
    This is exactly what I'm thinking about. – dymv Aug 16 '11 at 06:34
3

To all those who are having trouble with UINavigationBar custom backgrounds in iOS5, do this in the corresponding viewDidLoad methods:

#if defined(__IPHONE_5_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_5_0
if ([self.navigationController.navigationBar respondsToSelector:@selector( setBackgroundImage:forBarMetrics:)]){
    [self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"TitleBar"] forBarMetrics:UIBarMetricsDefault];
}
#endif

Notice that in my case, the background image was named "TitleBar". You can put whatever your custom background image name is.

animuson
  • 53,861
  • 28
  • 137
  • 147
aloha
  • 1,561
  • 13
  • 26
  • for some reason the closing #endif just won't sit inside the code bracket. Sorry for garbled #endif. – aloha Nov 26 '11 at 21:20
  • thanks, that is working! And don't forget to create @2x image – Nekto Dec 02 '11 at 15:54
  • the #define is preprocessor macro. Its useful only during compile. So if you are compiling for iOS5 and later, it will work. If you are compiling for pre-iOS5, the #ifdef will return false and that part of code will not be compiled. The #ifdef is good to have in case you compile your code across multiple iOS versions ,including pre-ios5, mostly to check for any compatibility issues. – aloha Dec 03 '11 at 13:05
  • There is no need for static #if #endif wrapper because you are verifying dynamically if the class responds to selector. – dmitri Dec 28 '12 at 03:43
2

The problem you'll have is that if you use a navigation controller, the title of each page will overlay your custom navbar. If your navbar contains a logo or the name of your app, this is obviously unacceptable.

You could set the title of each view in your navigation stack to blank, but some views force a title that you can't do anything about (like the photo picker). So you might want to create an alternate navbar image with the same color or design as your logo navbar, but with a blank area to make room for overlaid titles.

To switch navbar images at will, add a property to your app delegate to hold the name of the navbar image and replace the first line of the first example above with these two:

    YourAppDelegate* theApp = (YourAppDelegate*)[[UIApplication sharedApplication] delegate];
    UIImage* image = [UIImage imageNamed:theApp.navBarName];

Then in the first view controller that you'll push onto the navigation stack, do something like this:

- (void)viewWillAppear:(BOOL)animated
{
    YourAppDelegate* theApp = (YourAppDelegate*)[[UIApplication sharedApplication] delegate];
    theApp.navBarName = @"navBar_plain";
}

Then in the root view controller, do the same thing but specify your logo-bearing navbar image, so it gets restored when the user navigates back to it and there is no conflicting title.

Oscar
  • 2,039
  • 2
  • 29
  • 39
2

Another approach is to Use UINavigationController's delegate. It doesn't require subclassing/overwriting the UINavigationBar class:

/*
 in the place where you init the navigationController:
 fixer = [[AENavigationControllerDelegate alloc] init];
 navigationController.delegate = fixer;
*/

@interface AENavigationControllerDelegate : NSObject <UINavigationControllerDelegate>
@end

@implementation AENavigationControllerDelegate

#define bgImageTag 143

- (void)navigationController:(UINavigationController *)navigationController 
   didShowViewController:(UIViewController *)viewController 
                animated:(BOOL)animated
{
     //this is for the future for implementing with the appearance api:

    if ([[navigationController navigationBar] respondsToSelector:@selector(setBackgroundImage:forBarMetrics:)])
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [[navigationController navigationBar] setBackgroundImage:[UIImage imageNamed:@"header-logo-bg.png"]
                                                   forBarMetrics:UIBarMetricsDefault];
        });
 }
    else
        {
        UIImageView* imageView = (UIImageView*)[navigationController.navigationBar viewWithTag:bgImageTag];
        if(!imageView)
        {
            UIImage *image = [UIImage imageNamed:@"header-logo-bg.png"];
            imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
            imageView.tag = bgImageTag;
        }

        [navigationController.navigationBar insertSubview:imageView atIndex:0];        
    }
}

@end

https://gist.github.com/1184147

amosel
  • 661
  • 8
  • 10
2

In iOS5, zPosition value (of UINavigationBar's most depth layer) is changed. So if you change that zPosition, the old way works.

eg.

UINavigationBar *_bar = navigationController.navigationBar;

// Insert ImageView    
UIImage *_img = [UIImage imageNamed:@"navibar.png"];
UIImageView *_imgv = [[[UIImageView alloc] initWithImage:_img] autorelease];
_imgv.frame = _bar.bounds;

UIView *v = [[_bar subviews] objectAtIndex:0];
v.layer.zPosition = -FLT_MAX;

_imgv.layer.zPosition = -FLT_MAX+1;
[_bar insertSubview:_imgv atIndex:1];

This script handle view's layer, so You should import QuartzCore.

pablasso
  • 2,479
  • 2
  • 26
  • 32
U_Akihir0
  • 21
  • 1
1

Here is an alternative solution that lets you use your own custom subclass of UINavigationBar:

https://gist.github.com/1253807

1

As Apple itself has said, it is not correct to override methods in Categories. So the best way to customize the background of UINavigarionBar is subclassing and override -(void)drawInRect: method.

@implementation AppNavigationBar
- (void)drawRect:(CGRect)rect
{
    UIImage *patternImage = [UIImage imageNamed:@"image_name.png"];
    [patternImage drawInRect:rect];
}

To use this customized UINavigationBar it should be set as navigationBar property of your UINavigationBarController. As you know this property is readonly. So what should be done is:

- (void)viewDidLoad
{
    [super viewDidLoad];

    AppNavigationBar *nav = [AppNavigationBar new];
    [self setValue:nav forKey:@"navigationBar"];
}

It works for both iOS 5 and 4.3.

Emad
  • 669
  • 8
  • 21
0

You can subclass UINavigationBar and enable it like this, since categories for drawRect won't work in iOS5 anymore

    navigationController = [[((^ {
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSKeyedArchiver archivedDataWithRootObject:navigationController]];
    [unarchiver setClass:[SAPHUINavigationBar class] forClassName:@"UINavigationBar"];
    [unarchiver setClass:[UIViewController class] forClassName:NSStringFromClass([navigationController.topViewController class])];
    return unarchiver;
})()) decodeObjectForKey:@"root"] initWithRootViewController:navigationController.topViewController];
Pascalius
  • 14,024
  • 4
  • 40
  • 38
0

For a static view (no animation at all), I use the default iOS setBackgroundImage

But when I have a view that's animated (resize most likely), I create a custom UIImageView and add it to the navigationBar so that I have more flexibility over it

The thing is if you just add it, it will get on top of the buttons and the titleView, so I manually save a copy of most of subviews, remove them from parent view, add my imageView and than add all the subviews back

This works for me

    UIImageView *navBackground = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"navigationBackgroundSample.jpg"]];
    UIView *tempTitleView = [[self.navigationBar.subviews objectAtIndex:1] autorelease];
    [[self.navigationBar.subviews objectAtIndex:1] removeFromSuperview];
    [self.navigationBar addSubview:navBackground];
    [self.navigationBar addSubview:tempTitleView];
    self.navigationBar.clipsToBounds = YES;
    [navBackground release];

In this case, I don't have buttons and I found out that my titleView is at index 1, if you have buttons, they should be around somewhere in the subviews array of navigationBar

I don't know what's at index 0, I don't know if this can work around the case you have text title neither...

Andrew Barber
  • 39,603
  • 20
  • 94
  • 123
Keo Strife
  • 1,416
  • 3
  • 15
  • 23