聊聊UINavigationBar

作者: 一剑孤城 | 来源:发表于2017-07-16 11:04 被阅读394次

    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的层级关系,到时候,出了问题或者想要实现一些系统没有的效果就容易的多了。并且,一些第三方库封装的很好,可以直接用,我们就不要重复造轮子了。😄

    相关文章

      网友评论

        本文标题:聊聊UINavigationBar

        本文链接:https://www.haomeiwen.com/subject/wlexhxtx.html