码农的日常之iOS开发iOS Developer

UINavigationController结构分析以及使用

2017-02-22  本文已影响198人  Mark_Guan

一. 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];

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

上一篇下一篇

猜你喜欢

热点阅读