聊聊UINavigationBar

UINavigationBar是苹果系统自带的,一个很方便使用的导航栏,并且在同一个UINavigationController的控制器栈里面,共享一个UINavigationBar,可以保持统一性,但是,也因为是共用一个UINavigationBar,所以,只要控制器栈里面的某一个控制器修改了UINavigationBar会影响所有的控制器,这也算一个弊端。

1.为什么要聊聊UINavigationBar?😵

在做项目时,比较蛋疼的就是透明导航栏和非透明导航栏的切换问题,特别是在侧滑返回时,上一个页面可以透过导航栏看到下一个页面,造成很奇怪的显示。所以,因为踩坑太多,需要了解一下,导航栏到底是个什么东东。(PS: 这个问题其实只要当前页面的view充满整个屏幕,就不能透过导航栏看见下一个页面了。)

2.先来解剖一下UINavigationBar的层级

这里只涉及到iOS8~iOS10,由于没有iOS8以下的模拟器,也没有真机(😭),而且现在大部分手机系统都在iOS8以上了,iOS11也只是测试版,不稳定,先不管。下面进入正题:(PS: 下面都是居于iPhone6)


UINavigationBar层级.png

(1)iOS8/iOS9中UINavigationBar的层级结构如下:

UINavigationBar
    —— _UINavigationBarBackground
            —— _UIBackdropView  
            —— _UIBackdropEffectView
            —— UIImageView
    —— UINavigationItemView
    —— UINavigationButton
    —— _UINavigationBarBackIndicatorView

看一下这都是什么类???

UINavigationBar->UIView 
frame = (0 20; 375 44); opaque = NO; autoresize = W

_UINavigationBarBackground->_UIBarBackgroundImageView->UIImageViewframe = (0 -20; 375 64); autoresize = W; userInteractionEnabled = NO

_UIBackdropView->UIView 
frame = (0 0; 375 64); opaque = NO; autoresize = W+H; userInteractionEnabled = NO

_UIBackdropEffectView->UIView 
frame = (0 0; 375 64); clipsToBounds = YES; opaque = NO; autoresize = W+H; userInteractionEnabled = NO

UIImageView->UIView 
frame = (0 64; 375 0.5); userInteractionEnabled = NO 

UINavigationItemView->UIView
frame = (170.5 8; 34 27); opaque = NO; userInteractionEnabled = NO

UINavigationButton->UIButton->UIControl->UIView
frame = (316 7; 51 30); opaque = NO

_UINavigationBarBackIndicatorView->UIImageView
frame = (8 11.5; 13 21); alpha = 0; opaque = NO; userInteractionEnabled = NO

上面可以得出几点:
1. UINavigationBar的frame是(0 20; 375 44),所以UINavigationBar和UIStatusBar并不是混在一起的。
2. _UINavigationBarBackground的frame是(0 -20; 375 64),并且是UIImageView类型,也就是我们改变导航栏的背景图片就是赋值给这个类。
3. UIImageView,不用多说,这个就是阴影图片,它的frame是(0 64; 375 0.5),从64开始算,也就是说,将UINavigationBar的clipToBounds=Yes就能屏蔽掉底部阴影线。
4. 导航栏的背景 _UINavigationBarBackground,标题UINavigationItemView,左右按钮UINavigationButton和返回图片_UINavigationBarBackIndicatorView都是在同一层级。
5. _UIBackdropView和_UIBackdropEffectView是给导航栏添加上毛玻璃效果的类。

(2)iOS10中UINavigationBar的层级结构如下:

UINavigationBar
    —— _UIBarBackground
            —— UIImageView
            —— UIVisualEffectView
            —— _UIVisualEffectBackdropView
            —— _UIVisualEffectFilterView
    —— UINavigationItemView
    —— UINavigationButton
    —— _UINavigationBarBackIndicatorView

iOS10中的导航栏和iOS8/iOS9的导航栏层级结构差不多,区别在于使用系统提供的毛玻璃类:

UIVisualEffectView->UIView
frame = (0 0; 375 64)

_UIVisualEffectBackdropView->_UIVisualEffectSubview->UIView
frame = (0 0; 375 64); autoresize = W+H; userInteractionEnabled = NO

_UIVisualEffectFilterView->_UIVisualEffectSubview->UIView
frame = (0 0; 375 64); autoresize = W+H; userInteractionEnabled = NO

3.推荐管理导航栏比较好用的第三方库


WRNavigationBar实现的原理是,把系统的backgroundImage设置为透明,然后自己在backgroundView添加背景,设置图片背景的时候添加imageView,设置颜色背景时,直接添加view设置背景颜色,透明则通过直接设置backgroundView的alpha值来表现。而底部的阴影分割线只是提供了隐藏和显示的方法,也是通过直接隐藏shadowImage来实现的,核心代码如下:

@implementation UINavigationBar (WRAddition)

// set navigationBar backgroundImage
- (void)wr_setBackgroundImage:(UIImage *)image
{
    [self.backgroundView removeFromSuperview];
    self.backgroundView = nil;
    if (self.backgroundImageView == nil)
    {
        // add a image(nil color) to _UIBarBackground make it clear
        [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
        self.backgroundImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), kWRNavBarBottom)];
        // _UIBarBackground is first subView for navigationBar
        [self.subviews.firstObject insertSubview:self.backgroundImageView atIndex:0];
    }
    self.backgroundImageView.image = image;
}

// set navigationBar barTintColor
- (void)wr_setBackgroundColor:(UIColor *)color
{
    [self.backgroundImageView removeFromSuperview];
    self.backgroundImageView = nil;
    if (self.backgroundView == nil)
    {
        // add a image(nil color) to _UIBarBackground make it clear
        [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
        self.backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), kWRNavBarBottom)];
        // _UIBarBackground is first subView for navigationBar
        [self.subviews.firstObject insertSubview:self.backgroundView atIndex:0];
    }
    self.backgroundView.backgroundColor = color;
}

// set _UIBarBackground alpha (_UIBarBackground subviews alpha <= _UIBarBackground alpha)
- (void)wr_setBackgroundAlpha:(CGFloat)alpha
{
    UIView *barBackgroundView = self.subviews.firstObject;
    barBackgroundView.alpha = alpha;
}


KMNavigationBarTransition实现的原理分解:
KMNavigationBarTransition主要的文件是两个category,分别是UINavigationController+KMNavigationBarTransitionUIViewController+KMNavigationBarTransition,其中UINavigationController+KMNavigationBarTransitionhook四个方法:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        KMSwizzleMethod([self class],
                        @selector(pushViewController:animated:),
                        @selector(km_pushViewController:animated:));
        
        KMSwizzleMethod([self class],
                        @selector(popViewControllerAnimated:),
                        @selector(km_popViewControllerAnimated:));
        
        KMSwizzleMethod([self class],
                        @selector(popToViewController:animated:),
                        @selector(km_popToViewController:animated:));
        
        KMSwizzleMethod([self class],
                        @selector(popToRootViewControllerAnimated:),
                        @selector(km_popToRootViewControllerAnimated:));
        
        KMSwizzleMethod([self class],
                        @selector(setViewControllers:animated:),
                        @selector(km_setViewControllers:animated:));
    });
}

UIViewController+KMNavigationBarTransitionhook了两个方法:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        KMSwizzleMethod([self class],
                        @selector(viewWillLayoutSubviews),
                        @selector(km_viewWillLayoutSubviews));
        
        KMSwizzleMethod([self class],
                        @selector(viewDidAppear:),
                        @selector(km_viewDidAppear:));
    });
}

以push为例,KMNavigationBarTransition在km_pushViewController:animated:方法里做了以下操作:

- (void)km_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (!disappearingViewController) {
        return [self km_pushViewController:viewController animated:animated];
    }
    if (!self.km_transitionContextToViewController || !disappearingViewController.km_transitionNavigationBar) {
    [disappearingViewController km_addTransitionNavigationBarIfNeeded];
    }
    if (animated) {
        self.km_transitionContextToViewController = viewController;
        if (disappearingViewController.km_transitionNavigationBar) {
            disappearingViewController.km_prefersNavigationBarBackgroundViewHidden = YES;
        }
    }
    return [self km_pushViewController:viewController animated:animated];
}

在即将消失的controller,也就是push的上一个controller添加了一个navigationBar,并且将系统的navigationBarBackground隐藏:

- (void)km_addTransitionNavigationBarIfNeeded {
    if (!self.isViewLoaded || !self.view.window) {
        return;
    }
    if (!self.navigationController.navigationBar) {
        return;
    }
    [self km_adjustScrollViewContentOffsetIfNeeded];
    UINavigationBar *bar = [[UINavigationBar alloc] init];
    bar.barStyle = self.navigationController.navigationBar.barStyle;
    if (bar.translucent != self.navigationController.navigationBar.translucent) {
        bar.translucent = self.navigationController.navigationBar.translucent;
    }
    bar.barTintColor = self.navigationController.navigationBar.barTintColor;
    [bar setBackgroundImage:[self.navigationController.navigationBar backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault];
    bar.shadowImage = self.navigationController.navigationBar.shadowImage;
    [self.km_transitionNavigationBar removeFromSuperview];
    self.km_transitionNavigationBar = bar;
    [self km_resizeTransitionNavigationBarFrame];
    if (!self.navigationController.navigationBarHidden && !self.navigationController.navigationBar.hidden) {
        [self.view addSubview:self.km_transitionNavigationBar];
    }
}

viewWillLayoutSubviews方法里做了同样的操作,把即将push的controller添加navigationBar,然后隐藏系统的navigationBarBackground:

- (void)km_viewWillLayoutSubviews {
    id<UIViewControllerTransitionCoordinator> tc = self.transitionCoordinator;
    UIViewController *fromViewController = [tc viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [tc viewControllerForKey:UITransitionContextToViewControllerKey];
    
    if ([self isEqual:self.navigationController.viewControllers.lastObject] && [toViewController isEqual:self] && self.navigationController.km_transitionContextToViewController) {
        if (self.navigationController.navigationBar.translucent) {
            [tc containerView].backgroundColor = [self.navigationController km_containerViewBackgroundColor];
        }
        fromViewController.view.clipsToBounds = NO;
        toViewController.view.clipsToBounds = NO;
        if (!self.km_transitionNavigationBar) {
            [self km_addTransitionNavigationBarIfNeeded];
            
            self.km_prefersNavigationBarBackgroundViewHidden = YES;
        }
        [self km_resizeTransitionNavigationBarFrame];
    }
    if (self.km_transitionNavigationBar) {
        [self.view bringSubviewToFront:self.km_transitionNavigationBar];
    }
    [self km_viewWillLayoutSubviews];
}

viewDidAppear里面将自己添加的navigationBar移除,显示系统的navigationBar:

- (void)km_viewDidAppear:(BOOL)animated {
    if (self.km_transitionNavigationBar) {
        self.navigationController.navigationBar.barTintColor = self.km_transitionNavigationBar.barTintColor;
        [self.navigationController.navigationBar setBackgroundImage:[self.km_transitionNavigationBar backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault];
        [self.navigationController.navigationBar setShadowImage:self.km_transitionNavigationBar.shadowImage];
        
        UIViewController *transitionViewController = self.navigationController.km_transitionContextToViewController;
        if (!transitionViewController || [transitionViewController isEqual:self]) {
            [self.km_transitionNavigationBar removeFromSuperview];
            self.km_transitionNavigationBar = nil;
            self.navigationController.km_transitionContextToViewController = nil;
        }
    }
    self.km_prefersNavigationBarBackgroundViewHidden = NO;
    [self km_viewDidAppear:animated];
}

整体流程就是,先添加自定义的navigationBar,隐藏系统的navigationBar,等push完成就移除自定义的navigationBar,显示系统的navigationBar。

总结:

navigationBar是一个让人又爱又恨的东西,不过理解了navigationBar的层级关系,到时候,出了问题或者想要实现一些系统没有的效果就容易的多了。并且,一些第三方库封装的很好,可以直接用,我们就不要重复造轮子了。😄

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容