iOS Swift5从0到1系列(三):学习UINavigationController(1)

一、前言

上篇,我们仿了京东的底部导航栏,显示了5个页面(UIViewController),它的作用你可以理解为页面之间的切换,每个页面都属于一级页面;而一级页面一般只是一些吸引用户的的主要流量入口,实际的功能页面都是通过这些一级页面的入口导航到下一级或是更深一级的页面(从用户角度来看,核心重要的页面最多不超过三级,否则层级太深,用户可能就没兴趣继续点下去了,除了『确认订单』和『支付』页面)。

N级页面之间的切换,不会再涉及到底部导航控制器,它没那功能;在 iOS 中,具体页面间导航功能的就是我们今天要学习的 UINavigationController,即导航控制器,它能够实现不同页面之间的进入与退出。

二、导航控制器(UINavigationController)

导航控制器就是负责页面切换的,同样,我们先来看看官方源码注释:

UINavigationController manages a stack of view controllers and a navigation bar.
导航控制器通过数据结构『栈』的方式来管理一组 ViewController,并且它自带导航栏。

It performs horizontal view transitions for pushed and popped views while keeping the navigation bar in sync.
默认情况下,push 操作(进入下一级页面)和 pop 操作(返回到上一级页面)的转场动画是一个平移,并且,导航控制器会保持导航栏(状态的)同步。

如果你觉得上面的注释看不懂也没关系,接下来我会分析,知道的朋友可以直接跳下一小节(不太建议,有些细节『老司机』也不一定掌握 -_-|||)。我们先来看看 UIKit.UINavigationController 源码,了解其内部的核心对象和方法。

2.1、UINavigationController 分析

精华注释.....

@available(iOS 2.0, *)
open class UINavigationController : UIViewController {
    // 构造器,需要一个 UIViewController 作为根视图控制器
    // 初始化时,它会将这个 UIViewController 以 push 且没有动画的方式放入栈中
    // 该 UIViewController 将是栈中第一个视图控制器
    public init(rootViewController: UIViewController) 
    
    // 默认使用平移动画进入下一个页面,如果当前的页面已在栈顶,则没有任何效果
    open func pushViewController(_ viewController: UIViewController, animated: Bool) 

    // 返回页面有三种方式:
    // 1. 返回上一级页面:即从哪里来的,回到哪里去
    open func popViewController(animated: Bool) -> UIViewController? 
    // 2. 返回到指定的页面:你可以使用该方法返回上一级面页,也可以直接返回到根页面
    // 对于返回到指定的页面,则该页面所在栈中之上的所有页面都将出栈并销毁(这些页面的出栈不可见)
    open func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? 
    // 3. 返回到第一个页面
    open func popToRootViewController(animated: Bool) -> [UIViewController]? 
    
    // 当前栈中存在的页面,我们可以通过遍历:
    // 1. pop操作来返回到我们想要返回的页面
    // 2. 通过个数来做一些判断操作(后面会用到)
    open var viewControllers: [UIViewController] 
    
    @available(iOS 7.0, *)
    // 这个没有英文注释,不过,我们都会用到
    // 我们也看到了它的出现是在 7.0 以后,而它的功能就是:(右)侧滑返回上一级而面的手势
    // 因为苹果手机屏幕越来越大了嘛,总是让用户点『返回』按钮来返回到上一级,体验不好嘛
    open var interactivePopGestureRecognizer: UIGestureRecognizer? { get }
    
    // 导航栏,管理 navigationItem,同样使用栈来管理(为啥?)
    // 因为每个 VC 都对应着一个 navigationItem
    open var navigationBar: UINavigationBar { get } 
    
    @available(iOS 3.0, *)
    // tool条,含有 title 和 image
    open var toolbar: UIToolbar! { get }
    
    // 其它请自行查看
    ......
}

以上列出的方法和成员都是我们将来会用的着的,且会经常打交道,不过,类似于 UITabBarController,基本上只会打交道一次,为啥?因为一般我们都会在封装在基类中,由基类来统一管理,如果每个页面自己管理,则很容易乱套(苹果自己对这块都是非常混乱),而我给大家分享的,都是通过实践来告诉大家避免踩坑的最终结果。

2.2、扩展 UINavigationController 分析

extension UIViewController {
    // 大家可以自行创建导航栏的外观(左、右按钮,标题,文字、图片、颜色和透明度)
    open var navigationItem: UINavigationItem { get } 
    // 这个属性默认是 Swift: false / OC: NO,但这个属性在遇到 UITabBarController 时会有用处
    open var hidesBottomBarWhenPushed: Bool 
    // 我们通过 navigationController 来 push / pop 操作
    open var navigationController: UINavigationController? { get } 
}

2.3、导航栏元素:UINavigationItem 分析

我们可以结合着如下图片来分析了解:

navigation-bar-elements.png
@available(iOS 2.0, *)
open class UINavigationItem : NSObject, NSCoding {
    // 标题,默认 nil,在 UI层级的最顶层
    // 可以通过:Debug View Hierachy 查看视图的层级
    open var title: String?
    // 可以自定义标题视图(比如:放张图片),只有它在 UI最顶层才有效
    open var titleView: UIView?

    // 返回键 (左边按钮)
    open var backBarButtonItem: UIBarButtonItem?
    
    // leftBarButtonItems 和 rightBarButtonItems 在以前只是一个单一按钮,现在管理着一组按钮;
    // 这两分别对应着每个数组中的第一个元素
    @available(iOS 5.0, *)
    // 左按钮
    open var leftBarButtonItems: [UIBarButtonItem]?

    @available(iOS 5.0, *)
    // 右按钮
    open var rightBarButtonItems: [UIBarButtonItem]?
}

主要就是以上元素,如果在这里你没有任何疑惑,那么我是非常疑惑的!为啥?你没发现有两个元素都能控制『左侧按钮』么?分别是:

  • backBarButtonItem;
  • leftBarButtonItem;


如果同时设置了这两会有什么结果?

\color{red}{官方没有说,我们都是通过实践得出的真理:}

前提:previousVC 是上一个页面,nextVC 是下一个页面,当发生 push 时,有如下规则:

  1. 如果 nextVC 的 leftBarButtonItem != nil,那么将在 navigationBar 的左边显示 nextVC 指定的 leftBarButtonItem;
  2. 如果 nextVC 的 leftBarButtonItem == nil,previousVC 的 backBarButtonItem != nil,那么将在 navigationBar 的左边显示 previousVC 指定的 backBarButtonItem;
  3. 如果两者都为 nil 则:
  • 3.1. nextVC 的 navigationItem.hidesBackButton = YES,那么 navigationBar 将隐藏左侧按钮;
  • 3.2. 否则 navigationBar的左边将显示系统提供的默认返回按钮;


我们从以上规则中发现:

  1. leftBarButtonItem 的优先级比 backBarButtonItem 要高;
  2. backBarButtonItem 是来自上一个页面,如果当前 VC 是第一个页面,那么它没有上一个页面,也就没有 backBarButtonItem;
  3. leftBarButtonItem 是来自当前页面,与上个页面无关,因此,如果当前 VC 是第一个页面,那么设置了 leftBarButtonItem 就会很奇怪;

因此,请注意上面的左侧按钮规则,千万不要同时设置,虽然有优先级保证不会显示两个按钮,但是你的显示逻辑可能就不一样了!

分析完了这么多,我们用一张大图来总结一下:

UINavigationController.png

三、UINavigationController 的使用

3.1、使用方式

它的使用很简单,逃脱不开的模式,即 window.rootViewController 的设置有以下三种:

  1. 直接设置;
// AppDelegate 中
window?.rootViewController = UINavigationController(rootViewController: XXXViewController())
  1. UITabBarController 嵌套 UINavigationController;
// AppDelegate 中
window?.rootViewController = MainTabBarController()

// MainTabBarController 中
let xxx = UINavigationController(rootViewController: XXXViewController())
xxx.tabBarItem.title = "xxx"
viewControllers = [xxx, ......]
  1. UINavigationController 嵌套 UITabBarController(可能再嵌套 UINavigationController,就和第2种差不多了);
// AppDelegate 中
window?.rootViewController = UINavigationController(rootViewController: MainTabBarController())

// 可能再嵌套如下
// MainTabBarController 中
let xxx = UINavigationController(rootViewController: XXXViewController())
xxx.tabBarItem.title = "xxx"
viewControllers = [xxx, ......]

通过以上方式,我们能发现,常用的模式不是第 1 种,就是第 2 种。

3.2、实战出真理

基于我们上一篇的例子,我们采用第 2 种方式来使用。

  • 简单改造如下
wrap-with-uinavigation-controller.png
  • 修改 HomeViewController
homevc.png
  • 运行模拟器
run-simu.png

我们的导航栏(带标题)就出来了,这里需要注意一点:iOS 7.0 后,改为了扁平风格,这里的导航栏也就变成的半透明效果!

3.3、页面跳转 push & pop

  • 修改我们的 HomeViewController
tap.png
  • 点击 label,push 到下一级页面
push.png
  • 跳转稳定后,如下图
back.png

四、总结

本篇只是分析了导航控制器和最基本的使用,下一篇,我们会根据实际的场景及需求(上小节最后一张图,我们可以看到,push 到下级页面,底部 tabbar 仍旧显示,虽然少数 app 会这么做,但大多数 app 都会隐藏),结合着 UITabBarController 和 UINavigationController 来讲述我在实际工作中是如何来配置的。

推荐阅读更多精彩内容