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];
}

如果想在透明导航栏间平滑的切换 可以参考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];

9. 全屏手势滑动

- (void)viewDidLoad {
    [super viewDidLoad];    
    //设置手势代理
    self.interactivePopGestureRecognizer.enabled = NO;
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.interactivePopGestureRecognizer.delegate action:NSSelectorFromString(@"handleNavigationTransition:")];
    [self.view addGestureRecognizer:pan];
    pan.delegate = self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
    return self.viewControllers.count>1;
}

10.返回到指定控制器

方法一:

   [self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:2] animated:YES];

方法二:

 for (UIViewController *temp in self.navigationController.viewControllers) {
    if ([temp isKindOfClass:[要跳转回的控制器 class]]) {
        [self.navigationController popToViewController:temp animated:YES];
    }
}

10.点击TabBarItem直接push操作 比如跳转到登录界面

第一步:在自定义TabBarController中遵守 UITabBarControllerDelegate协议
第二步: self.delegate = self;
第三步:实现 - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController方法

- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
    NSLog(@"--tabbaritem.title--%@", viewController.tabBarItem.title);

    //这里我判断的是当前点击的tabBarItem的标题
    if ([viewController.tabBarItem.title isEqualToString:@"我的"]) {
        //如果用户ID存在的话,说明已登陆
        if (USER_ID) {
            return YES;
        }
        else
        {
            //跳到登录页面
            HPLoginViewController *login = [[HPLoginViewController alloc] init];
            //隐藏tabbar
            login.hidesBottomBarWhenPushed = YES;
            [((UINavigationController *)tabBarController.selectedViewController) pushViewController:login animated:YES];
            
            return NO;
        }
    } else{
        return YES;
    }
}

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

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

推荐阅读更多精彩内容