UINavigationController结构分析以及使用
一. 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];
}
如果想在透明导航栏间平滑的切换 可以参考GitHub和iOS透明导航栏的平滑过渡(进阶版)
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];
本文会持续更新,包括在工作中遇到的关于导航条的问题我都会记录再此......