美文网首页码农的日常之iOS开发iOS Developer
UINavigationController结构分析以及使用

UINavigationController结构分析以及使用

作者: Mark_Guan | 来源:发表于2017-02-22 15:48 被阅读198次

    一. UINavigationController的结构

    UINavigationController的结构大概分为如下三个区域:

    对应的更加立体的结构如下图:


    其中UILayoutContainerView对应的是self.navigationController.view ,我们可以打个断点来验证一下:

    UINavigationTransitionView转场的视图,也就是我们说的内容区
    UINavigationBar即导航栏
    其中ToolBar默认是隐藏的 我们可以手动显示 不过我们一般很少用到它:

    self.navigationController.toolbarHidden = NO;
    

    二. UINavigationTransitionView 内容区

    在iOS 7.0之前我们的导航栏是拟物化风格的,导航条是不透明的,内容区是在导航栏下紧挨着的(Y值从64开始)
    但是从iOS 7.0以后 我们的导航栏变成了扁平化风格,导航栏是透明的了,也就是说ViewController默认使用全屏布局

    为了更好的过渡,苹果从iOS 7.0以后新增了几个属性 我们一一为大家讲解

    1. edgesForExtendedLayout

    edgesForExtendedLayout是一个类型为UIRectEdge的属性,可以指定边缘要延伸的方向。因为iOS7之后鼓励全屏布局,它的默认值是UIRectEdgeAll,四周边缘均延伸,就是说如果即使视图中上有navigationBar,下有tabBar,那么视图仍会延伸覆盖到四周的区域。

    效果一:导航栏透明 并且内容全屏布局
    先来看一段非常普通的代码

    - (void)viewDidLoad {
       [super viewDidLoad];
       UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
       redView.backgroundColor = [UIColor redColor];
       [self.view addSubview:redView];    
    }
    

    运行效果如下图:



    可以看到红色的View的bounds是从屏幕左上角开始的,而不是导航栏左下方.

    其实等价于

        //导航栏透明 并且使用全屏布局
        self.navigationController.navigationBar.translucent = YES;
        self.edgesForExtendedLayout = UIRectEdgeAll;
        ....
    

    效果二:导航栏透明 非全屏布局
    那么如果此时我们想让redView的bounds从导航栏的左下方开始该如何操作呢?其实我们只需要改动一句代码即可:

    //不让View延展到整个屏幕
    self.edgesForExtendedLayout = UIRectEdgeNone;
    

    效果图如下:


    效果三:导航栏不透明 非全屏布局

    如果我们将导航栏设置为不透明效果会如何呢?

    - (void)viewDidLoad {
        [super viewDidLoad];   
        self.navigationController.navigationBar.translucent = NO;
        UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        redView.backgroundColor = [UIColor redColor];
        [self.view addSubview:redView];
    }
    

    可以看到我们将navigationBar.translucent设置为NO之后 控制器自动变为非全屏布局了,也就是等价于
    self.edgesForExtendedLayout = UIRectEdgeNone;
    

    效果四:导航栏不透明 全屏布局

    如果导航栏不透明但是又要实现全屏布局的效果 该如何操作呢?
    此时只需要将extendedLayoutIncludesOpaqueBars设置为YES即可:

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.navigationController.navigationBar.translucent = NO;
        self.extendedLayoutIncludesOpaqueBars = YES;
        UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        redView.backgroundColor = [UIColor redColor];
        [self.view addSubview:redView];
    }
    

    2. extendedLayoutIncludesOpaqueBars

    首先我们来看看官方对该属性的定义:

    // Defaults to NO, but bars are translucent by default on 7_0.
    @property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); 
    

    扩展布局是否包括不透明的Bars,默认为NO

    苹果这样做其实是很人性化的,如果bars不透明的情况下,再使扩展布局到bars的下方,这样感觉是毫无意义的,所以在bars不透明的情况下,默认不会延伸布局。

    3. automaticallyAdjustsScrollViewInsets

    从导航视图Push进来的以ScrollView 为主的视图,本来我们的cell是放在(0,0)的位置上的,但是考虑到导航栏、状态栏会挡住后面的主视图,所以系统会自动把我们的内容向下偏移64px(下方位置如果是tarbar则向上偏移49px

    我们以tableView为例子来验证一下,如下图:

    可以看到默认情况下Cell的显示是从导航栏下方开始的,我们可以打印一下tableView的信息查看一下,如下图:

    可以看到默认情况下系统将contentOffset下移了64

    那么,当我们不想让系统自动为我们下移时我们可以这样设置:

    if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    } else {
        self.automaticallyAdjustsScrollViewInsets = NO;
    }
    

    3. 导航控制器通过栈来管理子控制器

    UINavigationController 是通过栈的形式来管理控制器的,如何来验证这一点呢? 我们新建四个控制器,然后从第一个控制器依次 push到最后一个控制器 此时我们打印 po self.navigationController.viewControllers 如图:

    当我们打印当前可视控制器的时候,显示的就是栈顶的控制器,如图:

    当然我们从pushViewController:animated:popViewController:animated:这两个方法的描述也可以看出来导航栏的运作原理.

    三. UINavigationBar 导航栏区域

    1. UINavigationBar

    UINavigationBar继承自 UIView , 它主要用来管理导航栏的items的,我们可以看到它里面有一个items的数组:

    @property(nullable,nonatomic,copy) NSArray<UINavigationItem *> *items;
    
    

    UINavigationController一样,它也是通过栈来管理items的,而且和self.navigationController.viewControllers是一一对应的:

    如下图:

    2. UINavigationItem

    UINavigationItem继承自NSObject 而不是UIView,所以他是一个模型而不是一个视图,我们可以使用self.navigationItem来获取当前页面导航栏上显示的全部信息,包括title、titleView 、leftBarButtonItem、rightBarButtonItem、backBarButonItem

    因为它是一个模型所以当导航栏 push一个控制器的时候,他是时时刷新变化的,展示的是当前控制器的Item信息。 如果我们想让导航栏有一个固定不变的控件的话 我们可以向 UINavigationBar中添加一个子控件即可:

    UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 150, 30)];
        redView.backgroundColor = [UIColor redColor];
        [self.navigationController.navigationBar addSubview:redView];
    

    这样每个页面都包含了这个控件



    3. 导航栏透明效果

    比较暴力的方式:

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
        //处理导航栏有条线的问题
        [self.navigationController.navigationBar setShadowImage:[UIImage new]];
    }
    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
        [self.navigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
        [self.navigationController.navigationBar setShadowImage:nil];
    }
    

    当然我们也可以逐个去遍历navigationBar的子视图,然后改变他们的透明度

    // 设置导航栏背景透明度
    - (void)setNavigationBackgroundAlpha:(CGFloat)alpha {
        
        if (!self.navigationController.navigationBar.isTranslucent) return;
        
        // 导航栏背景透明度设置
        UIView *barBackgroundView;// _UIBarBackground
        UIImageView *backgroundImageView;// UIImageView
        UIView *backgroundEffectView;// UIVisualEffectView
        
        if (@available(iOS 10.0, *)) {//
            barBackgroundView = [self.navigationController.navigationBar.subviews objectAtIndex:0];//_UIBarBackground
            backgroundImageView = [barBackgroundView.subviews objectAtIndex:0];//UIImageView
            if (backgroundImageView != nil && backgroundImageView.image != nil) {
                barBackgroundView.alpha = alpha;
            } else {
                backgroundEffectView = [barBackgroundView.subviews objectAtIndex:1];//backgroundEffectView
                if (backgroundEffectView != nil) {
                    backgroundEffectView.alpha = alpha;
                }
            }
            
        }else{
            for (UIView *view in self.navigationController.navigationBar.subviews) {
                if ([view isKindOfClass:NSClassFromString(@"_UINavigationBarBackground")]) {
                    barBackgroundView = view;
                    barBackgroundView.alpha = alpha;
                    break;
                }
            }
            for (UIView *otherView in barBackgroundView.subviews) {
                if ([otherView isKindOfClass:NSClassFromString(@"UIImageView")]) {
                    backgroundImageView = (UIImageView *)otherView;
                    backgroundImageView.alpha = alpha;
                }else if ([otherView isKindOfClass:NSClassFromString(@"_UIBackdropView")]) {
                    backgroundEffectView = otherView;
                    backgroundEffectView.alpha = alpha;
                }
            }
        }
        
        // 对导航栏下面那条线做处理
        self.navigationController.navigationBar.clipsToBounds = alpha == 0.0;
    }
    

    在使用的时候我们可以:

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        [self setNavigationBackgroundAlpha:0.0];
    }
    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
        [self setNavigationBackgroundAlpha:1.0];
    }
    

    如果想在透明导航栏间平滑的切换 可以参考GitHubiOS透明导航栏的平滑过渡(进阶版)

    4. 导航栏隐藏

    在项目中经常碰到首页顶部是无限轮播,需要靠最上面显示.然后push到下一个页面的时候是需要导航栏的

    -(void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:YES];
        [self.navigationController setNavigationBarHidden:YES animated:animated];
        
    }
    -(void)viewWillDisappear:(BOOL)animated
    {
        self.navigationController.navigationBarHidden = NO;
        [super viewWillDisappear:animated];
    }
    

    5. UIBarButtonItem设置间距


    如上图 ,当导航条上有多个Item的时候,如果我们想调节两个Item之间的距离,让他们的距离更长该如何操作呢?其实我们只需要添加一个UIBarButtonSystemItemFixedSpace样式的Item即可:
        UIBarButtonItem *helpBtn = [[UIBarButtonItem alloc] initWithTitle:@"帮助" style:UIBarButtonItemStylePlain target:self action:nil];
        UIBarButtonItem *flexBtn = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:self action:nil];
        flexBtn.width = 45;
        UIBarButtonItem *moreBtn = [[UIBarButtonItem alloc] initWithTitle:@"更多" style:UIBarButtonItemStylePlain target:self action:nil];
        
        self.navigationItem.rightBarButtonItems = @[moreBtn,flexBtn,helpBtn];
    

    效果图如下:

    6. 变更返回图片

    如果我们想变更导航条上的返回图标:

        UIImage *backImg = [[UIImage imageNamed:@"arrow"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
        self.navigationController.navigationBar.backIndicatorImage = backImg;
        self.navigationController.navigationBar.backIndicatorTransitionMaskImage = backImg;
    

    效果如下:


    此时如果我们想让返回图标后面的文字消失该怎么做呢?

     [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];
    

    该方法在iOS11的时候会出现返回图标下沉效果,如下图:


    此时我们可以:

    [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(-100, 0) forBarMetrics:UIBarMetricsDefault];
    

    这样就正常了,如图:


    7. 设置文字颜色和大小

     [navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor redColor],NSFontAttributeName:[UIFont systemFontOfSize:25]}];
    

    8. 设置背景图片

    [navigationBar setBackgroundImage:[UIImage imageNamed:] forBarMetrics:UIBarMetricsDefault];
    

    本文会持续更新,包括在工作中遇到的关于导航条的问题我都会记录再此......

    相关文章

      网友评论

        本文标题:UINavigationController结构分析以及使用

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