Adding a Background Image to UIN…

来源:互联网 发布:战舰世界欧根亲王数据 编辑:程序博客网 时间:2024/04/28 20:14

Updated 2011/10/18: As ofiOS 5, Apple has officially added support for setting backgroundimages in UINavigationBars. To do this, all you need to do isexecute the setBackgroundImage:forBarMetrics: methodof UINavigationBar.

UINavigationBar *navBar = [[self navigationController] navigationBar];UIImage *backgroundImage = [UIImage imageNamed:@"nav-bar-background-normal"];[navBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];

However, if you want to support both iOS 4 and iOS 5, you willneed to conditionally call the code above on iOS 5 and the code Idescribe below in iOS 4. This is easy to do, and my sampleproject has been updated to work on both versions of iOS.

Toward the end of the development phase for the first release ofEpicure,Adam Betts startedsending me stellar design mockups. Many of his ideas were quiteeasy to implement. However, something as simple as adding abackground image to a UINavigationBar was much harder than it should have been. You'dhope that as a class integral to iPhone UI development,UINavigationBar would have a setBackgroundImage:method. Unfortunately, it does not (at least not as of iPhone OS3.1).

In the rest of this post, I will describe how, with a littlecleverness, we can in fact configure our UINavigationBar to haveany background image we desire. And to whet your appetite a little,this is how the final product will look:

Epicure's Navigation Bar

Before we dive into the code, it is important to understand thetechniques we will be using. When augmenting core Cocoa classes,there are a few different ways one can approach the situation.First (and most simple) is subclassing. It would be great if wecould subclass UINavigationBar and then just configure our UINavigationController to use our custom image. Unfortunately,the navigationBar property of UINavigationControlleris read-only and there is no way to set it duringinitialization.

Another way we can modify core Cocoa classes is to use Objective-C categories. These allow us to quickly add newmethods to existing classes. By using a category, we could easilyadd our own setBackgroundImage: method and call itwhenever we create a UINavigationBar or UINavigationController.This method would just add a UIImageView) as a subview to the navigation bar and then sendthat subview to the back of its superview.

However, if you try this you will quickly see that it only kindof works. The UIImageView is definitely visible and even initiallyat the correct z-index. However, once you push or pop a viewcontroller onto the navigation controller's stack, you will seethat the background image is no longer in the background. Instead,it blocks out the title and navigation bar buttons. This isdefinitely not what we want.

What we do want is a way to ensure that no matter what happens,the UIImageView we add is always in the background. To do this, wecan inject our own code into the sendSubviewToBack:and insertSubview:atIndex: methods of UIView. And in order to do this, we can use a technique calledmethodswizzling.

At its heart, method swizzling allows us to target particularmethods of existing classes and then override or augment them.There are many existing implementations for swizzling out there, soI would definitely recommend reading about them on the CocoaDev wiki. In the code thatfollows, I will use a very simple implementation that will workjust fine in our situation.

So let's jump into the code. First, we create a single classcalled SCAppUtils with a single method:

SCAppUtils.h
#import <UIKit/UIKit.h>#define kSCNavBarImageTag 6183746#define kSCNavBarColor [UIColor colorWithRed:0.54 green:0.18 blue:0.03 alpha:1.0]@interface SCAppUtils : NSObject{}+ (void)customizeNavigationController:(UINavigationController *)navController;@end
SCAppUtils.m
#import "SCAppUtils.h"@implementation SCAppUtils+ (void)customizeNavigationController:(UINavigationController *)navController{    UINavigationBar *navBar = [navController navigationBar];    [navBar setTintColor:kSCNavBarColor];    if ([navBar respondsToSelector:@selector(setBackgroundImage:forBarMetrics:)])    {        [navBar setBackgroundImage:[UIImage imageNamed:@"navigation-bar-bg.png"] forBarMetrics:UIBarMetricsDefault];    }    else    {        UIImageView *imageView = (UIImageView *)[navBar viewWithTag:kSCNavBarImageTag];        if (imageView == nil)        {            imageView = [[UIImageView alloc] initWithImage:                        [UIImage imageNamed:@"navigation-bar-bg.png"]];            [imageView setTag:kSCNavBarImageTag];            [navBar insertSubview:imageView atIndex:0];            [imageView release];        }    }}@end

Notice how we set the tint color to something that makes sensefor our image. Then, we add our background image (only if it isn'talready there) and ensure it has a z-index of 0 and a tag that noone else is likely using. This tag will be how we access the imageview later. Also notice how we don't even need to use a category todo this. UINavigationBar is a subclass of UIView, so we can justadd the background view directly.

The reason we create a utility method instead of overriding acore UINavigationBar method like drawRect: is becausewe really do not want to override every navigation bar in our app.We want to selectively determine which ones have the new backgroundimage. Otherwise, you get into nasty situations where thenavigation bar of your UIImagePickerController also has your custom background image.iPhone apps have been rejected for less.

Next, we need to use a category combined with swizzling toaugment UINavigationBar's sendSubviewToBack: andinsertSubview:atIndex:.

UINavigationBar+SCBackgroundImage.h
#import <UIKit/UIKit.h>@interface UINavigationBar (SCBackgroundImage)- (void)scInsertSubview:(UIView *)view atIndex:(NSInteger)index;- (void)scSendSubviewToBack:(UIView *)view;@end
UINavigationBar+SCBackgroundImage.m
#import "UINavigationBar+SCBackgroundImage.h"#import "SCAppUtils.h"@implementation UINavigationBar (SCBackgroundImage)- (void)scInsertSubview:(UIView *)view atIndex:(NSInteger)index{    [self scInsertSubview:view atIndex:index];    UIView *backgroundImageView = [self viewWithTag:kSCNavBarImageTag];    if (backgroundImageView != nil)    {        [self scSendSubviewToBack:backgroundImageView];    }}- (void)scSendSubviewToBack:(UIView *)view{    [self scSendSubviewToBack:view];    UIView *backgroundImageView = [self viewWithTag:kSCNavBarImageTag];    if (backgroundImageView != nil)    {        [self scSendSubviewToBack:backgroundImageView];    }}@end

This code is a little confusing, and most developers unfamiliarwith swizzling will immediately point at it and declare "infiniteloop"! However, with the way that swizzling works, we will swap thepointers to the original methods with our 'sc' counterparts. Thus,the methods you see above will actually become the realimplementations for the sendSubviewToBack: andinsertSubview:atIndex: selectors, and callingscInsertSubview:index: orscSendSubviewToBack: directly will actually invoke theoriginal methods defined by Apple. So to say it one more time aslightly different way, if you call [myNavBarsendSubviewToBack:otherView], you will actually be callingmy method above, which will then call [myNavBarscSendSubviewToBack:otherView], which is now Apple'simplementation of the method.

Phew.

But let's not get ahead of ourselves. Until we actually performthe swizzling, none of the magical stuff I just described willhappen. So let's define a utility method for swizzling:

SCAppUtils.h
#import <Foundation/Foundation.h>@interface SCClassUtils : NSObject{}+ (void)swizzleSelector:(SEL)orig ofClass:(Class)c withSelector:(SEL)new;@end
SCAppUtils.m
#import "SCClassUtils.h"#if OBJC_API_VERSION >= 2#import <objc/runtime.h>#else#import <objc/objc-class.h>#endif@implementation SCClassUtils+ (void)swizzleSelector:(SEL)orig ofClass:(Class)c withSelector:(SEL)new;{    Method origMethod = class_getInstanceMethod(c, orig);    Method newMethod = class_getInstanceMethod(c, new);    if (class_addMethod(c, orig, method_getImplementation(newMethod),                        method_getTypeEncoding(newMethod)))    {        class_replaceMethod(c, new, method_getImplementation(origMethod),                            method_getTypeEncoding(origMethod));    }    else    {        method_exchangeImplementations(origMethod, newMethod);    }}@end

This code is beyond the scope of this post. If you would like tounderstand how it works, I again refer you to this page on method swizzling.

Finally, we need to call ourswizzleSelector:ofClass:withSelector: method toactually perform the swizzling. We can do this by adding some codeto our main.m file:

main.m
#import <UIKit/UIKit.h>#import "SCClassUtils.h"int main(int argc, char *argv[]){    [SCClassUtils swizzleSelector:@selector(insertSubview:atIndex:)                          ofClass:[UINavigationBar class]                     withSelector:@selector(scInsertSubview:atIndex:)];    [SCClassUtils swizzleSelector:@selector(sendSubviewToBack:)                          ofClass:[UINavigationBar class]                     withSelector:@selector(scSendSubviewToBack:)];    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    int retVal = UIApplicationMain(argc, argv, nil, nil);    [pool release];    return retVal;}

And there you have it! That should be all you need to do to adda custom background image to your UINavigationBars. Just make surethat you call [SCAppUtilscustomizeNavigationController:myNavController] whenever youcreate a UINavigationController object. What this exercise originallyhelped me realize is that Objective-C is an incredibly powerfullanguage; even if Apple does not give you the hooks you need, youcan most likely do anything you want by being resourceful.

You can see all of this code in action bydownloading this sampleproject on GitHub.

0 0
原创粉丝点击