一、在项目开发中NavigationBar设置遇到的坑
在平时的开发中,我们往往会遇到这样的需求,两个ViewController的NavigationBar颜色不同、透明度不同或者有的隐藏有的不隐藏,当两个ViewController进行push或pop操作时,那么你可能会看到下面现象:
- 两个ViewController的NavigationBar颜色不同,push/pop时颜色切换不和谐。 push/pop颜色切换不和谐
- 两个ViewController的NavigationBar隐藏设置不一样,push/pop时隐藏NavigationBar切换不和谐。 push/pop时隐藏NavigationBar切换不和谐
很丑有木有O(≧口≦)O,强迫症接受不了有木有O(≧口≦)O。
导致出现上述问题的原因是navigationBar只有一个,改变navigationBar样式一定会影响其他ViewController的显示。
二、寻找解决方案
隐藏与显示的bug解决比较简单,因为苹果已经做好了,出现上述问题的原因是,没有使用对的方法。
隐藏NavigationBar苹果提供了两个方法:[navigationController setNavigationBarHidden:]和[navigationController setNavigationBarHidden:animated:]。第一个方法无动画隐藏navigationBar,第二个方法可以控制是否动画隐藏navigationBar。解决上述bug只需如下代码:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:NO animated:animated];
//不要使用下面方法,下面方法会导致隐藏和不隐藏的viewController转场时出现bug
// [self.navigationController setNavigationBarHidden:NO];
}
OK,隐藏与显示的bug解决了。效果如下:
正确的使用方法解决bug
下面解决颜色的问题。在很多的APP都可以看到不同颜色的navigationBar的转场,但这些APP都完美的解决了颜色问题,比如微信。所以就要看看微信是如何处理的呢,这个时候就用到了一个款神奇,Mac软件Reveal,它可以看到在手机中安装的APP页面层级,至于如何使用请移步:使用Reveal查看任意App的技巧。
通过Reveal看到了微信的页面层次,如下图
观察微信的层级发现,微信的navigationBar是透明的,ViewController顶部有一个view来充当navigationBar背景色,如上图左右两边的发现ViewController和小程序ViewController的顶部都有一个view,左边是黑字的右边是白色的,这样每个viewController的navigationBar的背景色就可以单独设置。
三、有了指导方向,开始动手搬砖
解决方法好像很简单,只需要在viewController.view的顶部加上一个barBgView就可以了(内心暗喜:这种代码我两分钟就可以敲完,啊哈哈哈ψ(`∇´)ψ)。
那么开始战斗吧,啊哈哈! 哈利路亚!德玛西亚!赐给我码神的力量吧ヽ(`Д´)ノヽ(`Д´)ノヽ(`Д´)ノ!
战斗没开始就发现遇到了坑,难道我要每个viewController里都写一遍加入barBgView的代码?不行viewController太多写起来太累;那写一个继承自viewController的父类,然后让所有viewController继承父类,不行那样还是每个viewController都要改;有没有让新项目改动极少的代码就可以实现的方法呢?答案是有的,只需要Category和黑科技Method Swizzling即可。
首先建两个UIViewController的Category.
第一个为UIViewController+CFYNavigationBarTransition.h
// UIViewController CFYNavigationBarTransition
@interface UIViewController (CFYNavigationBarTransition)
/**
设置导航栏是否隐藏
@param hidden 隐藏
@param animated 动画
*/
- (void)cfy_setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated;
@end
第二个位UINavigationController+CFYNavigationBarTransition_Public.h
@interface UIViewController (CFYNavigationBarTransition_Public)
/**
设置导航栏背景色
@param color 背景色
*/
- (void)cfy_setNavigationBarBackgroundColor:(UIColor *)color;
/**
设置背景图片
@param image 背景图
*/
- (void)cfy_setNavigationBarBackgroundImage:(UIImage *)image;
/**
设置导航栏透明度
@param alpha 透明度
*/
- (void)cfy_setNavigationBarAlpha:(CGFloat)alpha;
/**
bar背景色
*/
@property (readonly) UIColor *cfy_navigationBarBackgroundColor;
/**
bar透明度
*/
@property (readonly) CGFloat cfy_navigationBarAlpha;
@end
两个都是UIViewController的Category,以public结尾的文件中是对外提供公开的方法和属性,也就是用户可以使用的方法和属性,另个则是放私有方法和属性。
两个category中方法和属性的实现都在UIViewController+CFYNavigationBarTransition.m中实现,实现逻辑加在了注释中
@interface UIViewController ()
/**
cfy_navBarBgView,这个view就是核心,改变navigationBar颜色其实是改变cfy_navBarBgView的背景色
*/
@property (nonatomic, strong) UIView *cfy_navBarBgView;
/**
用来判断view是否加载
*/
@property (nonatomic, assign) BOOL cfy_viewAppeared;
/**
保存navigationBar颜色
*/
@property (nonatomic, strong) UIColor *cfy_navigationBarBackgroundColor;
/**
保存navigationBar颜色透明度
*/
@property (nonatomic, assign) CGFloat cfy_navigationBarAlpha;
@end
@implementation UIViewController (CFYNavigationBarTransition)
/**
在load中,swizzle四个方法viewDidLoad、viewWillLayoutSubviews、viewDidAppear:、viewDidDisappear:。
*/
+(void)load {
CFYSwizzleMethod(self, @selector(viewDidLoad), @selector(cfy_viewDidLoad));
CFYSwizzleMethod(self, @selector(viewWillLayoutSubviews), @selector(cfy_viewWillLayoutSubviews));
CFYSwizzleMethod(self, @selector(viewDidAppear:), @selector(cfy_viewDidAppear:));
CFYSwizzleMethod(self, @selector(viewDidDisappear:), @selector(cfy_viewDidDisappear:));
}
/**
在viewDidLoad中添加cfy_navBarBgView
*/
- (void)cfy_viewDidLoad {
[self cfy_viewDidLoad];
// 如果存在navigationController则添加cfy_navBarBgView
if (self.navigationController) {
[self cfy_addNavBarBgView];
}
}
- (void)cfy_viewDidAppear:(BOOL)animated {
[self cfy_viewDidAppear:animated];
self.cfy_viewAppeared = YES;
}
- (void)cfy_viewDidDisappear:(BOOL)animated {
[self cfy_viewDidDisappear:YES];
self.cfy_viewAppeared = NO;
}
/**
在viewWillLayoutSubviews中对cfy_navBarBgView进行处理,使cfy_navBarBgView能在不同环境正确显示
*/
- (void)cfy_viewWillLayoutSubviews {
[self cfy_viewWillLayoutSubviews];
// 当前viewController没navigationController,直接退出
if (!self.navigationController) {
return;
}
/**
self.navigationController.navigationBar隐藏了,做一些处理。
如果在navigationBar隐藏时,旋转屏幕,这时如果不处理后并return,而是走下面的代码,那么并不能正确的获取到cfy_navBarBgView的frame。
所以在这里直接将cfy_navBarBgView的宽度设置成屏幕看度,其他不变保持cfy_navBarBgView在隐藏前的状态,这样在从竖屏切换到横屏显示时不会出现一些视觉上的bug
*/
if (self.navigationController.navigationBar.hidden) {
CGRect rect = self.cfy_navBarBgView.frame;
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y, CFYScreenWidth, rect.size.height);
return;
}
// 获取navigationBar的backgroundView
UIView *backgroundView = [self.navigationController.navigationBar valueForKey:@"_backgroundView"];
// 如果没有则return
if (!backgroundView) {
return;
}
// 获取navigationBar的backgroundView在self.view中的位置,这个位置也就是cfy_navBarBgView所在的位置。
CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
// 出现rect.origin.x < 0,情况只有在页面刚push出来并且navigationBar隐藏的时候。
// 这个时候讲rect.origin.y上移rect.size.height,使cfy_navBarBgView也隐藏
// 目的是防止在navigationBar.hidden=NO时出现动画显示错误
if (rect.origin.x < 0) {
rect.origin.y = 0 - rect.size.height;
}
// cfy_navBarBgView的x固定0
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y, rect.size.width, rect.size.height);
// 设置当前view的clipsToBounds = NO,原因是,self.view.top可能是从navigationBar.bottom开始,如果clipsToBounds = YES,则cfy_navBarBgView无法显示
self.view.clipsToBounds = NO;
// 将cfy_navBarBgView移到self.view最顶端,防止被其他view遮盖
[self.view bringSubviewToFront:self.cfy_navBarBgView];
}
#pragma mark - 公开方法 -
/**
设置导航栏背景色
@param color 背景色
*/
- (void)cfy_setNavigationBarBackgroundColor:(UIColor *)color {
self.cfy_navigationBarBackgroundColor = color;
if (self.navigationController) {
self.cfy_navBarBgView.backgroundColor = color;
}
}
/**
设置背景图片
@param image 背景图
*/
- (void)cfy_setNavigationBarBackgroundImage:(UIImage *)image {
// 后续版本加入
}
/**
设置导航栏透明度
@param alpha 透明度
*/
- (void)cfy_setNavigationBarAlpha:(CGFloat)alpha {
self.cfy_navigationBarAlpha = alpha;
if (self.navigationController) {
self.cfy_navBarBgView.alpha = alpha;
}
}
#pragma mark - 私有方法 -
/**
设置导航栏是否隐藏
@param hidden 隐藏
@param animated 动画
*/
- (void)cfy_setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated {
if (self.navigationController) {
// 这里只在cfy_navBarBgView隐藏时使用了动画,原因是cfy_navBarBgView显示时系统自动给加上了动画(这很神奇)
if (hidden && self.cfy_viewAppeared && animated && !self.navigationController.navigationBar.hidden) {
// 在cfy_navBarBgView隐藏,并且view已经Appeared,并且有动画,并且navigationBar不是已经隐藏了时就进行动画
CGRect rect = self.cfy_navBarBgView.frame;
[UIView animateWithDuration:0.2 animations:^{
// 动画时向上运动
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y - rect.size.height, rect.size.width, rect.size.height);
} completion:^(BOOL finished) {
// 动画完成后cfy_navBarBgView隐藏
self.cfy_navBarBgView.hidden = hidden;
}];
} else {
self.cfy_navBarBgView.hidden = hidden;
}
}
}
/**
添加navigationBar背景view
*/
- (void)cfy_addNavBarBgView {
if (!self.isViewLoaded) {
return;
}
if (!self.navigationController) {
return;
}
if (!self.navigationController.navigationBar) {
return;
}
// 获取NavigationBar的BackgroundView在当前view中的位置
CGRect rect = [self cfy_getNavigationBarBackgroundViewRect];
// 初始化
UIView *navBarBgView = [[UIView alloc] initWithFrame:CGRectMake(0, rect.origin.y, rect.size.width, rect.size.height)];
[self.view addSubview:navBarBgView];
// 判断有没有设置颜色
if (self.cfy_navigationBarBackgroundColor) {
navBarBgView.backgroundColor = self.cfy_navigationBarBackgroundColor;
} else {
// 默认是白色
navBarBgView.backgroundColor = [UIColor whiteColor];
self.cfy_navigationBarBackgroundColor = [UIColor whiteColor];
}
// 设置透明度,默认为1
navBarBgView.alpha = self.cfy_navigationBarAlpha;
// 是否隐藏
navBarBgView.hidden = self.navigationController.navigationBar.isHidden;
// 保存
[self setCfy_navBarBgView:navBarBgView];
}
/**
获取navigationBar._backgroundView在self.view中的frame
@return _backgroundView的frame
*/
- (CGRect)cfy_getNavigationBarBackgroundViewRect {
UIView *backgroundView = [self.navigationController.navigationBar valueForKey:@"_backgroundView"];
if (!backgroundView) {
return CGRectZero;
}
CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
return rect;
}
#pragma mark - getter/setter -
-(UIView *)cfy_navBarBgView {
UIView *navBarBgView = objc_getAssociatedObject(self, _cmd);
if (nil == navBarBgView) {
[self cfy_addNavBarBgView];
}
return navBarBgView;
}
- (void)setCfy_navBarBgView:(UIView *)navBarBgView {
objc_setAssociatedObject(self, @selector(cfy_navBarBgView), navBarBgView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)cfy_viewAppeared {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setCfy_viewAppeared:(BOOL)viewAppeared {
objc_setAssociatedObject(self, @selector(cfy_viewAppeared), @(viewAppeared), OBJC_ASSOCIATION_ASSIGN);
}
- (UIColor *)cfy_navigationBarBackgroundColor {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setCfy_navigationBarBackgroundColor:(UIColor *)navigationBarBackgroundColor {
objc_setAssociatedObject(self, @selector(cfy_navigationBarBackgroundColor), navigationBarBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(CGFloat)cfy_navigationBarAlpha {
NSNumber *alpha = objc_getAssociatedObject(self, _cmd);
if (!alpha) {
[self setCfy_navigationBarAlpha:1.];
return 1.;
}
return [alpha floatValue];
}
- (void)setCfy_navigationBarAlpha:(CGFloat)navigationBarAlpha {
objc_setAssociatedObject(self, @selector(cfy_navigationBarAlpha), @(navigationBarAlpha), OBJC_ASSOCIATION_ASSIGN);
}
@end
OK,主要功能完成,上面代码中还有几个问题:
- 问题1:在设置[navigationController setNavigationBarHidden:animated:]和[navigationController setNavigationBarHidden:]方法是应该对cfy_navBarBgView进行操作;
- 问题2:没有设置NavigationBar为透明,不设置成透明,前面的工作都白做了。
那么对navigationController也建一个Category,并swizzle需要的方法。代码如下:
UIViewController+CFYNavigationBarTransition.m
@interface UIViewController ()
/**
cfy_navBarBgView,这个view就是核心,改变navigationBar颜色其实是改变cfy_navBarBgView的背景色
*/
@property (nonatomic, strong) UIView *cfy_navBarBgView;
/**
用来判断view是否加载
*/
@property (nonatomic, assign) BOOL cfy_viewAppeared;
/**
保存navigationBar颜色
*/
@property (nonatomic, strong) UIColor *cfy_navigationBarBackgroundColor;
/**
保存navigationBar颜色透明度
*/
@property (nonatomic, assign) CGFloat cfy_navigationBarAlpha;
@end
@implementation UIViewController (CFYNavigationBarTransition)
/**
在load中,swizzle四个方法viewDidLoad、viewWillLayoutSubviews、viewDidAppear:、viewDidDisappear:。
*/
+(void)load {
CFYSwizzleMethod(self, @selector(viewDidLoad), @selector(cfy_viewDidLoad));
CFYSwizzleMethod(self, @selector(viewWillLayoutSubviews), @selector(cfy_viewWillLayoutSubviews));
CFYSwizzleMethod(self, @selector(viewDidAppear:), @selector(cfy_viewDidAppear:));
CFYSwizzleMethod(self, @selector(viewDidDisappear:), @selector(cfy_viewDidDisappear:));
}
/**
在viewDidLoad中添加cfy_navBarBgView
*/
- (void)cfy_viewDidLoad {
[self cfy_viewDidLoad];
// 如果存在navigationController则添加cfy_navBarBgView
if (self.navigationController) {
[self cfy_addNavBarBgView];
}
}
- (void)cfy_viewDidAppear:(BOOL)animated {
[self cfy_viewDidAppear:animated];
self.cfy_viewAppeared = YES;
}
- (void)cfy_viewDidDisappear:(BOOL)animated {
[self cfy_viewDidDisappear:YES];
self.cfy_viewAppeared = NO;
}
/**
在viewWillLayoutSubviews中对cfy_navBarBgView进行处理,使cfy_navBarBgView能在不同环境正确显示
*/
- (void)cfy_viewWillLayoutSubviews {
[self cfy_viewWillLayoutSubviews];
// 当前viewController没navigationController,直接退出
if (!self.navigationController) {
return;
}
/**
self.navigationController.navigationBar隐藏了,做一些处理。
如果在navigationBar隐藏时,旋转屏幕,这时如果不处理后并return,而是走下面的代码,那么并不能正确的获取到cfy_navBarBgView的frame。
所以在这里直接将cfy_navBarBgView的宽度设置成屏幕看度,其他不变保持cfy_navBarBgView在隐藏前的状态,这样在从竖屏切换到横屏显示时不会出现一些视觉上的bug
*/
if (self.navigationController.navigationBar.hidden) {
CGRect rect = self.cfy_navBarBgView.frame;
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y, CFYScreenWidth, rect.size.height);
return;
}
// 获取navigationBar的backgroundView
UIView *backgroundView = [self.navigationController.navigationBar valueForKey:@"_backgroundView"];
// 如果没有则return
if (!backgroundView) {
return;
}
// 获取navigationBar的backgroundView在self.view中的位置,这个位置也就是cfy_navBarBgView所在的位置。
CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
// 出现rect.origin.x < 0,情况只有在页面刚push出来并且navigationBar隐藏的时候。
// 这个时候讲rect.origin.y上移rect.size.height,使cfy_navBarBgView也隐藏
// 目的是防止在navigationBar.hidden=NO时出现动画显示错误
if (rect.origin.x < 0) {
rect.origin.y = 0 - rect.size.height;
}
// cfy_navBarBgView的x固定0
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y, rect.size.width, rect.size.height);
// 设置当前view的clipsToBounds = NO,原因是,self.view.top可能是从navigationBar.bottom开始,如果clipsToBounds = YES,则cfy_navBarBgView无法显示
self.view.clipsToBounds = NO;
// 将cfy_navBarBgView移到self.view最顶端,防止被其他view遮盖
[self.view bringSubviewToFront:self.cfy_navBarBgView];
}
#pragma mark - 公开方法 -
/**
设置导航栏背景色
@param color 背景色
*/
- (void)cfy_setNavigationBarBackgroundColor:(UIColor *)color {
self.cfy_navigationBarBackgroundColor = color;
if (self.navigationController) {
self.cfy_navBarBgView.backgroundColor = color;
}
}
/**
设置背景图片
@param image 背景图
*/
- (void)cfy_setNavigationBarBackgroundImage:(UIImage *)image {
// 后续版本加入
}
/**
设置导航栏透明度
@param alpha 透明度
*/
- (void)cfy_setNavigationBarAlpha:(CGFloat)alpha {
self.cfy_navigationBarAlpha = alpha;
if (self.navigationController) {
self.cfy_navBarBgView.alpha = alpha;
}
}
#pragma mark - 私有方法 -
/**
设置导航栏是否隐藏
@param hidden 隐藏
@param animated 动画
*/
- (void)cfy_setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated {
if (self.navigationController) {
// 这里只在cfy_navBarBgView隐藏时使用了动画,原因是cfy_navBarBgView显示时系统自动给加上了动画(这很神奇)
if (hidden && self.cfy_viewAppeared && animated && !self.navigationController.navigationBar.hidden) {
// 在cfy_navBarBgView隐藏,并且view已经Appeared,并且有动画,并且navigationBar不是已经隐藏了时就进行动画
CGRect rect = self.cfy_navBarBgView.frame;
[UIView animateWithDuration:0.2 animations:^{
// 动画时向上运动
self.cfy_navBarBgView.frame = CGRectMake(0, rect.origin.y - rect.size.height, rect.size.width, rect.size.height);
} completion:^(BOOL finished) {
// 动画完成后cfy_navBarBgView隐藏
self.cfy_navBarBgView.hidden = hidden;
}];
} else {
self.cfy_navBarBgView.hidden = hidden;
}
}
}
/**
添加navigationBar背景view
*/
- (void)cfy_addNavBarBgView {
if (!self.isViewLoaded) {
return;
}
if (!self.navigationController) {
return;
}
if (!self.navigationController.navigationBar) {
return;
}
// 获取NavigationBar的BackgroundView在当前view中的位置
CGRect rect = [self cfy_getNavigationBarBackgroundViewRect];
// 初始化
UIView *navBarBgView = [[UIView alloc] initWithFrame:CGRectMake(0, rect.origin.y, rect.size.width, rect.size.height)];
[self.view addSubview:navBarBgView];
// 判断有没有设置颜色
if (self.cfy_navigationBarBackgroundColor) {
navBarBgView.backgroundColor = self.cfy_navigationBarBackgroundColor;
} else {
// 默认是白色
navBarBgView.backgroundColor = [UIColor whiteColor];
self.cfy_navigationBarBackgroundColor = [UIColor whiteColor];
}
// 设置透明度,默认为1
navBarBgView.alpha = self.cfy_navigationBarAlpha;
// 是否隐藏
navBarBgView.hidden = self.navigationController.navigationBar.isHidden;
// 保存
[self setCfy_navBarBgView:navBarBgView];
}
/**
获取navigationBar._backgroundView在self.view中的frame
@return _backgroundView的frame
*/
- (CGRect)cfy_getNavigationBarBackgroundViewRect {
UIView *backgroundView = [self.navigationController.navigationBar valueForKey:@"_backgroundView"];
if (!backgroundView) {
return CGRectZero;
}
CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
return rect;
}
#pragma mark - getter/setter -
-(UIView *)cfy_navBarBgView {
UIView *navBarBgView = objc_getAssociatedObject(self, _cmd);
if (nil == navBarBgView) {
[self cfy_addNavBarBgView];
}
return navBarBgView;
}
- (void)setCfy_navBarBgView:(UIView *)navBarBgView {
objc_setAssociatedObject(self, @selector(cfy_navBarBgView), navBarBgView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)cfy_viewAppeared {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setCfy_viewAppeared:(BOOL)viewAppeared {
objc_setAssociatedObject(self, @selector(cfy_viewAppeared), @(viewAppeared), OBJC_ASSOCIATION_ASSIGN);
}
- (UIColor *)cfy_navigationBarBackgroundColor {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setCfy_navigationBarBackgroundColor:(UIColor *)navigationBarBackgroundColor {
objc_setAssociatedObject(self, @selector(cfy_navigationBarBackgroundColor), navigationBarBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(CGFloat)cfy_navigationBarAlpha {
NSNumber *alpha = objc_getAssociatedObject(self, _cmd);
if (!alpha) {
[self setCfy_navigationBarAlpha:1.];
return 1.;
}
return [alpha floatValue];
}
- (void)setCfy_navigationBarAlpha:(CGFloat)navigationBarAlpha {
objc_setAssociatedObject(self, @selector(cfy_navigationBarAlpha), @(navigationBarAlpha), OBJC_ASSOCIATION_ASSIGN);
}
@end
至此所有的代码完工。
- 改变navigationBar的颜色,调用[viewController cfy_setNavigationBarBackgroundColor:bgColor]方法。
- 改变navigationBar的透明度,调用[viewController cfy_setNavigationBarAlpha:alpha]方法.
- 隐藏则直接调用UINavigationController中设置NavigationBar隐藏的方法
注意事项:不要设置NavigationBar的translucent为NO,原因是设置了translucent=NO,NavigationBar就不能透明了。
四、成果展示
竖屏效果横屏效果
五、代码地址
GitHub: CFYNavigationBarTransition
Cocoapods:pod 'CFYNavigationBarTransition'
网友评论
用这个库,跟MJRefresh控件一起用,做了一个导航栏从透明渐变为黑色的效果。但是,偶尔会出现状态栏透明之后,不随着导航栏颜色改变,一直保持透明。这个是跟MJRefresh有啥冲突吗?