一.iOS4中定制导航栏背景
在iOS4中通过重写UINavigationBar的drawRect:方法,可以修改导航栏的背景。
1.使用类别(Category)扩展重写drawRect:
@implementation UINavigationBar(CustomBackground)
- (void)drawRect:(CGRect)rect {
UIImage *image = [UIImage imageNamed:@"NavBar"];
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
return;
}
@end
通过类别重写后,其他代码不需做任何更改即可改变导航栏的背景。
2.通过创建UINavigationBar子类重写drawRect:
@implementation MyNavigationBar
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
UIImage *image = [UIImage imageNamed:@"NavBar"];
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
}
@end
一般不建议对UINavigationBar进行子类化,创建UINavigationBar子类后,则相应UINavigationController都需要将设置为使用导航栏子类。
在iOS5及之后的版本,UINavigationController提供了initWithNavigationBarClass:toolbarClass:方法在导航控制器中使用定制的导航栏和工具栏子类。
在iOS4版本中,可以通过XIB的方式设置UINavigationController中导航栏子类,具体见这里。
二. iOS5及以后版本中定制导航栏背景
从iOS5开始,UINavigationBar默认不再调用drawRect:方法,如 iOS SDK Release Notes for iOS 5.0中所述。
In iOS 5, the UINavigationBar, UIToolbar, and UITabBar implementations have changed so that the drawRect: method is not called 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. Apps can either:
Use the customization API for bars in iOS 5 and later, which is the preferred way.
Subclass UINavigationBar (or the other bar classes) and override drawRect: in the subclass.
iOS5版本中UINavigationBar类中不再调用drawRect:方法,但在UINavigationBar子类中drawRect:仍会被调用。所以上述通过类别(Category)扩展重写drawRect:的方法失效,而创建UINavigationBar子类的方法仍然有效,但仍然是不建议使用。
从iOS5版本来时,新增了可定制导航栏Appearance的一系列API,如可通过setBackgroundImage:forBarMetrics:设置单个导航栏的背景。
也可通过[UINavigationBar appearance]获取appearance代理来设置所有导航栏的背景,具体可见UIAppearance Protocol Reference
这样,在iOS5中可以使用Appearance API,在ViewDidLoad:中修改单个导航栏的背景
- (void)viewDidLoad
{
[super viewDidLoad];
UINavigationBar *navBar = self.navigationController.navigationBar;
if ([navBar respondsToSelector:@selector(setBackgroundImage:forBarMetrics:)]) {
[navBar setBackgroundImage:[UIImage imageNamed:@"NavBar"] forBarMetrics:UIBarMetricsDefault];
}
}
三.兼容iOS4和iOS5的处理方法
对于同时需要支持iOS4及iOS5版本的APP来说,则需要同时使用重写drawRect:和使用Appearance的方式,而且导航栏的背景图应该是可以随时配置的。所以我们将上述两种方法都集成到到类别(Category)扩展中,使用Associative References来存储设置的背景图。代码如下:
#import "UINavigationBar+CustomBackground.h"
#import <objc/runtime.h>
static char backgroundImageKey;
@implementation UINavigationBar (CustomBackground)
// iOS5 之前的版本调用
- (void)drawRect:(CGRect)rect {
UIImage *image = objc_getAssociatedObject(self, &backgroundImageKey);
if (!image) {
image = [UIImage imageNamed:@"NavBar"];
objc_setAssociatedObject(self, &backgroundImageKey, image, OBJC_ASSOCIATION_RETAIN);
}
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
return;
}
-(void)setBackgroundImage:(UIImage *)backgroundImage;
{
if ([self respondsToSelector:@selector(setBackgroundImage:forBarMetrics:)]) {
[self setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
}else{
objc_setAssociatedObject(self, &backgroundImageKey, backgroundImage, OBJC_ASSOCIATION_RETAIN);
[self setNeedsDisplay];
}
}
-(UIImage*)backgroundImage
{
if ([self respondsToSelector:@selector(backgroundImageForBarMetrics:)]) {
return [self backgroundImageForBarMetrics:UIBarMetricsDefault];
}else{
return objc_getAssociatedObject(self, &backgroundImageKey);
}
}
@end
增加上述代码到项目后,我们可以使用[navBar setBackgroundImage:image]设置导航栏背景图。
对于iOS4,此方法会将image保存到关联对象中,然后调用setNeedsDisplay要求导航栏重绘,在重绘调用drawRect:时新的导航栏背景就会生效;
对应iOS5及以上版本,则直接使用setBackgroundImage:forBarMetrics:设置背景图。
四.在不同页面切换导航栏背景
有时需要在同一个Navigation Controller下各个子页面使用不同的导航栏背景,但所对应的导航栏对象实际只有一个,这就涉及到导航栏背景图的保存、更改与恢复。比如有这样的需求,在Navigation Controller下有三个子页面:MainViewController、ViewController2、ViewController3,其中MainViewController和ViewController3的导航栏背景为默认值,即上述代码中的NavBar,ViewController2的导航栏背景图为BlackNavBar;在ViewController2上还可以present一个新页面PresentViewController。
MainViewController,ViewController3的导航栏背景为默认,其代码不用有任何修改。ViewController2的导航栏背景为新背景图,则需要做导航背景图的保存,设置及恢复操作
1.保存导航栏背景
在viewDidLoad方法中保存原始的导航栏背景。
@implementation ViewController2{
UIImage *savedNavBarImage;
}
- (void)viewDidLoad
{
[super viewDidLoad];
savedNavBarImage = [self.navigationController.navigationBar backgroundImage];
...
}
2.设置新导航栏背景
每次viewWillAppear:时设置新导航背景图。
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"BlackNavBar"]];
}
3.恢复导航栏背景
重点在于何时恢复导航栏背景,在Navigation Controller push一个新页面或者当前页面被pop的时候,需要恢复导航栏背景。而在当前页面上present一个新页面时不能修改导航栏。
当Navigation Controller pop或push时,在当前页面的viewWillDisappear:方法中Navigation Controller的viewControllers已更新,通过判断当前Navigation Controller的viewControllers的内容可以区分出当前页面消失时是在进行pop、push操作还是在进行present操作。
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
NSUInteger index = [self.navigationController.viewControllers indexOfObject:self];
if (index == NSNotFound || index == self.navigationController.viewControllers.count-2) {//pop 或者push
[self.navigationController.navigationBar setBackgroundImage:savedNavBarImage];
}
}
上述代码有一个假定,即在viewWillDisappear:时self.navigationController仍然指向当前的Navigation Controller,没有被置nil。但实际并非如此,在iOS4,iOS5版本中,以非动画的方式pop时(即调用popViewControllerAnimated:或popToRootViewControllerAnimated:方法时,传递参数为NO)在当前页面的viewWillDisappear:时self.navigationController为nil;在这种情况下需要通过其他方式获取到当前的Navigation Controller。
在上述情况下,尽管self.navigationController为nil,在self.view.superView仍然指向Navigation Controller中的view,我们可通过self.view.superView定位到其所属的viewController,即为当前的Navigation Controller。
通过view获取到其所属的viewContoller可通过向上逐级遍历nextResponder的方式实现,如下扩展UIView:
@implementation UIView (ViewController)
-(UIViewController*)viewController{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end
这样,上述恢复导航栏背景的代码可修改为:
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
UINavigationController *navController = self.navigationController;
//hack:ios5及之前版本在非动画方式pop时self.navigationController为nil,通过其他途径获取导航控制器
if (!navController) {
UIViewController *parentController = [self.view.superview viewController];
if ([parentController isKindOfClass:[UINavigationController class]]) {
navController = (UINavigationController*)parentController;
}
}
NSUInteger index = [navController.viewControllers indexOfObject:self];
if (index == NSNotFound || index == self.navigationController.viewControllers.count-2) {//pop 或者push
[navController.navigationBar setBackgroundImage:savedNavBarImage];
}
}
五.iOS6下的状态栏颜色
在iOS6下,如果statusBarStyle为UIStatusBarStyleDefault的话,则状态栏的颜色会自动随着导航栏的颜色变化而变化,其颜色为导航栏的平均颜色;如果修改StatusBarStyle为UIStatusBarStyleBlackOpaque或UIStatusBarStyleBlackTranslucent后就固定为不透明黑色和透明黑色,不再随导航栏变化了。
六.参考代码
https://github.com/xuguoxing/customNavigationBar
参考
- UINavigationController Class Reference
- UINavigationBar Class Reference
- UIViewController Class Reference
- iOS SDK Release Notes for iOS 5.0
- UINavigationBar’s drawRect is not called in iOS 5.0
- Set a custom subclass of UINavigationBar in UINavigationController programmatically
- Set background image of an UINavigationBar
- viewWillDisappear: Determine whether view controller is being popped or is showing a sub-view controller