前言
导航控制器作为我们日常开发使用最频繁的控件之一,其实有很多特性被我们所忽略。特别是布局惯用 storyboard 以来。
目录
-
- UINavigationController 属性和方法介绍
-
- UINavigationControllerDelegate
-
- UIViewController (UINavigationControllerItem)
-
- UIViewController (UINavigationControllerContextualToolbarItems)
一、 UINavigationController 属性和方法介绍 :
一些 iOS 8 后添加的特性展示,只需要设置属性就能实现的交互效果和 Safari 一致:
// iOS 5 后出现,使用自定义的 NavigationBar 和 toolbar 初始化一个导航控制器
// 且这两个参数可传 nil,当传 nil 时依然使用系统的 UINavigationBar 和 UIToolbar 初始化
- (instancetype)initWithNavigationBarClass:(nullable Class)navigationBarClass toolbarClass:(nullable Class)toolbarClass NS_AVAILABLE_IOS(5_0);
// 这是我们最常用的初始化方法,使用一个 rootViewController 初始化导航控制器
// 其实是把 rootViewController 压入堆栈底且没有动画
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
// 跳转到 viewController,该方法应该理解为把 viewController 压入堆栈
// animated 为 YES 则水平滑动过渡到 viewController,否则 viewController 直接显示,效果特别突兀
// 如果 viewController 已经在当前堆栈中,使用该方法会 crash,控制台会打印如下:
// reason: 'Pushing the same view controller instance more than once is not supported'
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;
这里引出一个问题:当我们 push 一个
[[UIViewController alloc] init]
创建的控制器的时候, 会"黑屏",开始我以为 push 到的这个 VC 没有创建 view,试着在 loadView 和 viewDidLoad 方法里打印 view 都是有值的。后来看到view 的 backgroundColor 的属性注释是 : // default is nil. Can be useful with the appearance proxy on custom UIView subclasses.
原来是 view 的背景色默认为 nil 。 (...) 这里又引出了另外一个问题,对象类型的属性默认值是 nil 而基本类型的变量默认是有值的 ?
// 返回上一个控制器,该方法应该理解为把栈顶的控制器弹出栈,返回值就是这个被弹出栈的控制器
// 因为堆栈里所有的控制器的 navigationController 属性都指向同一个导航控制器 ,所以不管该导航控制器在那里执行这个方法,都是把堆栈顶的控制器弹出
// 这里有点迷惑,该方法把这个控制器返回有什么意义,接下来这个控制器就要被销毁了 ️
- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated;
这里引出一个很重要的东西,只要我们拿到当前的导航控制器,我们就能在任何地方执行入栈和出栈操作,即控制器的跳转,自此我们不用在堆栈里的每个控制器的内部做 push 和 pop,仔细想想可以实现什么功能呢!
// 弹出堆栈里的控制器,直到 viewController 在堆栈顶
// 返回的数组存放被弹出堆栈栈的控制器
- (nullable NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated;
2018-05-29 22:51:45.611720+0800 HMTransitionAnimationDemo[68203:27655827] popToViewController 返回值打印: (
"",
""
)
2018-05-29 22:51:45.620069+0800 HMTransitionAnimationDemo[68203:27655827] dealloc self =
2018-05-29 22:51:46.122701+0800 HMTransitionAnimationDemo[68203:27655827] dealloc self =
这里又引出一个问题,堆栈的入栈顺序是
0neVC -> twoVC -> threeVC
,然后当 threeVC 在栈顶的时候,执行 popTo 到 oneVC 的操作,如果按栈--后进先出
的规则,应该是 threeVC 先出栈 twoVC 后出栈,而且对数组进行添加操作时默认是排在数组尾部的,但打印的结果是 twoVC 在 threeVC 的前面,且 dealloc 方法的执行,也是 twoVC 在 threeVC 的后面。
// 弹出,直到堆栈只剩下一个根控制器
// 返回值依然是被弹出栈的控制器数组
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated;
这里又引出一个问题:每次入栈都是
oneVC -> twoVC -> threeVC -> fourVC -> fiveVC
popToRoot 返回的控制器数组里顺序刚好相反,且 dealloc 的打印每次都是Two、Three、One、Four、Five
2018-05-29 23:16:21.232814+0800 HMTransitionAnimationDemo[68827:27697372] popToViewController 返回值打印: (
"",
"",
"",
"",
""
)
2018-05-29 23:16:21.245882+0800 HMTransitionAnimationDemo[68827:27697372] dealloc self =
2018-05-29 23:16:21.246273+0800 HMTransitionAnimationDemo[68827:27697372] dealloc self =
2018-05-29 23:16:21.246526+0800 HMTransitionAnimationDemo[68827:27697372] dealloc self =
2018-05-29 23:16:21.247744+0800 HMTransitionAnimationDemo[68827:27697372] dealloc self =
2018-05-29 23:16:21.754604+0800 HMTransitionAnimationDemo[68827:27697372] dealloc self =
// 导航栈顶的控制器
@property(nullable, nonatomic,readonly,strong) UIViewController *topViewController;
// 暂时未知
@property(nullable, nonatomic,readonly,strong) UIViewController *visibleViewController;
// 当前堆栈中所有的 viewController
@property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers;
// 设置当前堆栈中的控制器,为 viewControllers 数组中的控制器,可以理解为刷新堆栈中的控制器
// animated 参数为 YES 时,如果数组中最后的控制器和当前堆栈顶的控制器是同一个则不做任何动画,
// 如果是之前堆栈中不存在的控制器,则模拟一个入栈的动画,如果是之前堆栈里已经存在的控制器则模拟一个出栈的动画
- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);
这里发现了一个大问题,因为导航控制器本身有一个 viewControllers 的属性,所以它默认是会自动生成一个该属性的 set 方法的。当把上面的方法的 animated 传 NO 的时候,和调用 viewControllers 的 set 方法执行效果是一样的。
这里注意了,重点是传 YES 的时候,举个例子可能更容易理解,这里不好表述,比如当前堆栈中是 @[oneVC, twoVC, threeVC], 我拿 twoVC 放到一个新数组的末尾,把 threeVC 放到数组前面就像这样 @[threeVC, twoVC] , 只要 twoVC 在数组末尾,把 animated 传 YES ,页面会模拟出栈的动画,这时 threeVC 会 dealloc 的,而传 NO 或者直接调用 set 方法时,threeVC 不会 dealloc。
做了很多测试,总结出了规律,如果页面模拟了出栈动画,那么之前栈顶的那个控制必会 dealloc。当然如果之前堆栈里的控制器都没有添加到新设置的那个控制器数组里,则之前堆栈里的控制器都会 dealloc。(这个设置堆栈中新控制器的方法可以用来修改堆栈中控制器既有的顺序,但是貌似并没有什么业务场景需要,好像并没有什么卵用,(花了我一个小时的时间去总结规律,F**k)...)
// navigationBar 隐藏或显示(没有动画效果)
@property(nonatomic,getter=isNavigationBarHidden) BOOL navigationBarHidden;
// navigationBar 隐藏或显示,如果有动画则采用 UINavigationControllerHideShowBarDuration 的垂直过渡动画
- (void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated;
// 由控制器管理的 navigationBar,只读属性
// Pushing, popping or setting navigation items on a managed navigation bar is not supported.
@property(nonatomic,readonly) UINavigationBar *navigationBar;
// 导航控制器页面底部的 toolbar 默认是隐藏的
@property(nonatomic,getter=isToolbarHidden) BOOL toolbarHidden NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
// 动画隐藏或显示屏幕底部的 toolbar ,如果用有动画则采用 UINavigationControllerHideShowBarDuration 的垂直过渡动画
- (void)setToolbarHidden:(BOOL)hidden animated:(BOOL)animated NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
// 当呈现 actionSheet 时使用
@property(null_resettable,nonatomic,readonly) UIToolbar *toolbar NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
// 导航控制器的代理对象,协议方法内容下面会讲
@property(nullable, nonatomic, weak) id delegate;
// iOS 7 后出现的, pop 操作的交互手势,即导航条默认的左边缘滑动返回手势
// 设置它的 enable 可以控制该手势是否失效(左边缘滑动是否可以 pop 操作)
@property(nullable, nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,和 pushViewController:animated: 一样
// 这个方法是为了方法命名统一,在 storyBoard 里面控制器拉线跳转的时候,选 show 还是别的选项
- (void)showViewController:(UIViewController *)vc sender:(nullable id)sender NS_AVAILABLE_IOS(8_0);
// iOS 8 后出现的,当键盘出现时 navigationBar 将隐藏,
@property (nonatomic, readwrite, assign) BOOL hidesBarsWhenKeyboardAppears NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,在屏幕滑动时,navigationBar 和 toolBar 显示和隐藏且有垂直过渡效果,且 toolbar 上有 item 时才会隐藏和显示
// 其实很重要的一点,这个特性一般用来针对 tableView 的滚动来用,
// 如果当前的控制器是 UIViewController 或其子类,在它的 view 上添加 tableView 上下滚动,navigationBar 和 toolbar 是不会隐藏和显示的
// 如果当前控制器是 UITableViewController 或其子类,在它自带的 tableView 上下滑动时,navigationBar 和 toolbar 会隐藏和显示
@property (nonatomic, readwrite, assign) BOOL hidesBarsOnSwipe NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,只读属性,上下滑动时隐藏和显示 navigationBar 的手势
// 不要试图改变它的 delegate 或替换它
@property (nonatomic, readonly, strong) UIPanGestureRecognizer *barHideOnSwipeGestureRecognizer NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,横屏的时候隐藏 navigationBar 和 toolbar
// tap 屏幕的时候它们会重新显示
@property (nonatomic, readwrite, assign) BOOL hidesBarsWhenVerticallyCompact NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,当 tap 屏幕时,navigationBar 和 toolbar 隐藏和显示,且 toolbar 上有 item 时才会隐藏和显示
@property (nonatomic, readwrite, assign) BOOL hidesBarsOnTap NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// iOS 8 后出现的,只读属性,tap 屏幕时隐藏和显示 navigationBar 的手势
// 不要试图改变它的 delegate 或替换它
@property (nonatomic, readonly, assign) UITapGestureRecognizer *barHideOnTapGestureRecognizer NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// 与转场动画有关的两个 protocol,后面再分析
@protocol UIViewControllerInteractiveTransitioning;
@protocol UIViewControllerAnimatedTransitioning;
二、UINavigationControllerDelegate :
// 当 navigationController 进行 push、pop、设置堆栈里面新的控制器组的时候调用,一个是将要执行一个是执行完毕
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
// iOS 7 后出现的,设置 navigationController 支持的方向
- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
// iOS 7 后出现的,设置 navigationController 首选的方向
- (UIInterfaceOrientation)navigationControllerPreferredInterfaceOrientationForPresentation:(UINavigationController *)navigationController NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
// iOS 7 后出现的,与自定义转场动画有关的两个方法,后面再分析
- (nullable id )navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id ) animationController NS_AVAILABLE_IOS(7_0);
- (nullable id )navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
三、UIViewController (UINavigationControllerItem):
// navigationItem 是 UIViewController 的一个只读属性
// 且需要时才会创建,以便于定制导航条的外观
@property(nonatomic,readonly,strong) UINavigationItem *navigationItem;
// 当 push 操作时隐藏底部的 bar,比如隐藏 tabbar,默认为 NO
@property(nonatomic) BOOL hidesBottomBarWhenPushed __TVOS_PROHIBITED;
// 只读属性,堆栈中的每个控制器共有的 navigationController
@property(nullable, nonatomic,readonly,strong) UINavigationController *navigationController;
四、UIViewController (UINavigationControllerContextualToolbarItems):
// toolbar 上的按钮数组
// 上面我们知道 navigationController 底部有 toolbar 默认隐藏的
// 这里设置 toolbarItems 却是在 viewController 上,当 navigationController 的 toolbarHidden 为 YES 时,viewController 设置 toolbarItems 是不会显示的
@property (nullable, nonatomic, strong) NSArray<__kindof UIBarButtonItem *> *toolbarItems NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
- (void)setToolbarItems:(nullable NSArray *)toolbarItems animated:(BOOL)animated NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
// toolbarItems 设置方法
UIBarButtonItem *itemOne = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"refresh"] style:UIBarButtonItemStylePlain target:self action:@selector(one)];
[self setToolbarItems:@[itemOne] animated:YES];
由图可知,navigationBar 和 toolbar 平级,切属于 navigationController
UINavigationController.h 先写到这里,后面在分析与它相关的其他类。
navigationController 里面容易忽略的细节挺多,建议对 .h 文件逐行阅读。共勉。
-end-