导航栏隐藏 && 导航栏错乱

96
李国安
2016.08.23 15:56* 字数 2558

有感

....请允许我在文章开篇爆句粗口, 就这导航栏错乱这 Bug, 我真日了狗了. 自从测试小伙伴发现这个问题以来, 已经有几天的时间了, 就复现这个 Bug, 就花费了大笔时间. 调研了半天才终于把这个 Bug 复现了. 写这篇文章的目的, 一个是记录一下项目中遇到的疑难杂症, 另一个就是希望能帮助到那些同样被这些问题困扰的小伙伴们. 总之, 这一段就是吐槽一下. 废了那么多话, 开始正题吧. 先放出来一个 Demo.

Demo

先给大家 Demo 的下载地址 Click Here
简单的讲解一下这个 Demo:

  1. KeyWindowRootViewController 是一个UITabBarViewController
  2. UITabBarViewController 的每一个ViewController 都嵌套了一个MLNavigationController.
  3. 每一个ViewController 都继承自MLBaseViewController.
  4. MLBaseViewController.h 中, 声明了一个枚举类型MLNavigationHiddenType, 这个枚举类型来控制整个工程隐藏NavigationBar 的方式.
  5. MLBaseViewController.h 中还声明了一个全局变量hiddenType, 用这个变量来设置整个工程隐藏NavigationBar 的方式.
  6. 整个工程中, 只有MLUserHomePageViewController, MLMineViewControllerMLLoginViewControllerNavigationBar 处于隐藏状态.

OK, 差不多介绍到这里吧, 细节部分大家具体看代码吧.

Demo 中使用的隐藏方式

  1. [self.navigationController setNavigationBarHidden: YES];
  2. [self.navigationController setNavigationBarHidden: YES animated: NO];
  3. [self.navigationController setNavigationBarHidden: YES animated: YES];
  4. [self.navigationController setNavigationBarHidden: YES animated: animated];
  5. self.navigationController.delegate = self 这种方法在最后为大家讲解, 这其实才是真正的究极方法

其实NavigationBar的显示与隐藏其实很简单, 只需要在ViewControllerviewWillAppear 中隐藏 NavigationBar, 在试图控制器的 viewWillDisappear 中显示 NavigationBar就可以了, 但是仅仅这么做, 会带来一些 UI 上的 Bug, 其实我们也可以定义一些成员变量来控制 Bug 的产生, 但是很繁琐, 项目庞大了之后, 会导致代码极其不易维护。代码如下:

- (void) viewWillAppear:(BOOL)animated {
  [super viewWillAppear: animated];

  // 方法1
  [self.navigationController setNavigationBarHidden: YES];

  // 方法2
  [self.navigationController setNavigationBarHidden: YES animated: NO];

  // 方法3
  [self.navigationController setNavigationBarHidden: YES animated: YES];

  // 方法4
  [self.navigationController setNavigationBarHidden: YES animated: animated];
}

- (void) viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear: animated];

  // 方法1
  [self.navigationController setNavigationBarHidden: NO];

  // 方法2
  [self.navigationController setNavigationBarHidden: NO animated: NO];

  // 方法3
  [self.navigationController setNavigationBarHidden: NO animated: YES];

  // 方法4
  [self.navigationController setNavigationBarHidden: NO animated: animated];
}

文中将会涉及到的 Bug

__Bug__1: 没有导航栏的试图控制器没有导航栏的试图控制器之间的切换效果.
__Bug__2: 没有导航栏的控制器有导航栏的控制器 之间进行切换的效果.
__Bug__3: 没有导航栏的控制器 Present 一个 视图控制器 的效果.
__Bug__4: NavigationBar 错乱.
__Bug__5: UITabBarController 切换ViewController 的效果.

几种隐藏导航栏方法产生的问题

方法1 [self.navigationController setNavigationBarHidden: YES];

方法2 [self.navigationController setNavigationBarHidden: YES animated: NO];

先来说说方法1方法2, 其实这两个方法效果是几乎一样的, 全都是隐藏NavigaionBar并且不需要使用动画. 我们先来看一下效果, 下面这张动图是 没有导航栏的控制器没有导航栏的控制器 之间进行切换的效果:

没有NavigationBar 的两个 ViewController 切换效果

其实这个效果还算可以, 也没有什么非常不友好的 UI 效果, 但是接下来的动图, 就会给用户带来一些很不愉快的体验: Bug2没有导航栏的控制器有导航栏的控制器 之间进行切换的效果:

没有NavigationBar 和 有NavigationBar 的 ViewController 切换效果

可以清楚的看到, 在使用Pop 返回手势的时候, 右上角有一个非常明显的黑色区域, 效果非常不尽如人意, 其实不友好的地方不仅仅只有这一个地方, 看下图: Bug3 没有导航栏的控制器 Present 一个 视图控制器 的效果:

没有NavigationBar 的 ViewController Present 一个 ViewController

由于我们在 viewWillDisappear 方法中, 做了显示NavigationBar 的操作, 所以当我们点击登录按钮的一瞬间, 导航栏出现了. 这会给细心的用户带来一种非常匪夷所思的感觉, 同样非常的不友好. 但是如果仅仅如此的话, 可能有些小伙伴也就忍了, 也就懒得继续钻研下去了, 但是... 重点来了, 使用方法1方法2隐藏NavigationBar, 会带来一个非常不可思议的 Bug, 也就是本文题目中写道的, NavigationBar错乱的 Bug, Bug4 请仔细看下面这个动图(这个动图时间有点长, 所以质量下降了, 主要观察NavigationBar部分就可以了):

NavigationBar 错乱 Bug

我相信一定不止是我一个人遇到了这个问题, 在网上搜索了很多帖子, 确实也有不少的小伙伴遇到了这个问题, 我先来说一下这个问题的复现方法:

  1. 需要3个ViewController (至少3个, 更多也可以), 隐藏第一个ViewControllerNavigationBar(其实你隐藏哪个都可以, 我这里以第一个为例)
  2. push 进入ViewController2, 使用 iOS7后系统的右划返回手势, 这里注意一下, 手指右划到一半的时候, 取消Pop 动作, 此时页面依然停留在ViewController2当中, 然后再完整的做一次右划手势PopViewController1.
  3. 这个时候, 实际上这个NavigationContrllerNavigationBar 结构已经错乱了, 我们可以PushViewController3中去看一下NavigationBar 的情况. (无论是 PushViewContrller3 或者是 PushViewController4,ViewController5,ViewController6, 你会发现所有的ViewContrllerNavigationBar 都会非常神奇的一闪而过, 然后变成了 ViewController2NavigationBar)

我只想说: WTF, 好神奇的样子. 其实这个 Bug 复现起来并不是很困难, 难点在于:
你的测试小伙伴会给你提 Bug 说:

ViewController3的导航栏有问题, 但是这个问题非常偶然, 我也没有办法复现.

WTF, 你不复现我怎么办, 难道真的要我把需求放下, 来花大笔的时间来复现这个 Bug 么? T_T...
不过还好, 这个 Bug 我这边已经帮助大家复现出来了, 我相信, 一定会帮助到一些已经在风中凌乱了的小伙伴们( OK, 装逼结束, 其实是在 Google 上搜索到了这个小伙伴的文章后受到了启发, 不过这篇文章中, 并没有给出一个非常 Prefect 的解决办法).
好了, 废话不多说了, 接下来看看到底该怎么解决上面这个 Bug , 以及上面提到的 UI 不友好的地方吧, 接下来, 我们来看方法3.

方法3 [self.navigationController setNavigationBarHidden: YES animated: YES];

使用方法3来设置NavigationBar 的隐藏和出现, 经过测试, 解决了 Bug2 没有导航栏的控制器有导航栏的控制器 之间进行切换的效果, 看下图:

解决的 Bug

方法3确实解决了 Bug2, 其实方法3也同样的解决了NavigationBar 错乱的问题, 经测试, 隐藏NavigaionBar 的时候, 将动画属性置为YES, 就能解决NavigationBar 错乱的问题了, 但是这样做也是会付出一些代价的.

  • 首先: Bug3 没有导航栏的控制器 Present 一个 视图控制器 的效果, 并没有得到解决. (大家可以自行查看 Demo)
  • 其次: 将动画属性设置为 YES 之后, 会带来两个新的问题, 我将这两个问题定义为 Bug1Bug5.
    先来看看 Bug1, 在方法1方法2中的第一个动图, 没有导航栏的试图控制器没有导航栏的试图控制器之间的切换效果, 当动画属性置为YES后, 原本效果还不错的地方, 也产生了不友好的用户体验, 如下图:

带来的新的 Bug

...什么鬼..., 这NavigationBar 干嘛呢.... 路过一下是么? -.-!!! 体验非常不好, 再看下一个, TabBarItem 之间的切换效果: Bug5 UITabBarController 试图切换效果:

UITabBarController 切换的效果

好家伙, 这玩意...不看不知道,一看吓一跳啊....每次进入我的页面的时候, 都要哆嗦一下, 这感觉太不爽了.

小结: 仅仅将动画属性置为 YES, 还是不太科学, 那再来看看方法4

方法4 [self.navigationController setNavigationBarHidden: YES animated: animated];

和刚才一样, 先来说说 方法4所解决的 Bug 吧.

  1. 方法3一样, 方法4同样解决了 Bug2Bug4.
  2. 方法4还解决了 Bug5
    与语言表达有点麻烦, 来看看下面的动图, 看看方法4所解决的 Bug:
Auto_FixBug.gif

再来看看方法4所未能解决的问题吧. Bug1Bug3:

Auto_Bugs.gif

其实方法4, 已经做的很好了, 解决了大部分的 Bug, 但是我们可以看到依然存在这两个不友好的 Bug. 接下来就给大家介绍这个究极方法.

究极方法 self.navigationController.delegate = self

先来说说写法吧, 最开始的时候, 我是写了一个继承自UINavigationController 的类, 设置代理并遵守<UINavigationController>协议, .m 文件中的3个关键方法如下:

#pragma mark - ViewController Life Circle
#pargma mark -
#Pargma mark ViewDidLoad
- (void) viewDidLoad {
  [super viewDidLoad];

  // 1. 设置代理
  self.delegate = self;
}

#pragma mark - Private Methods
#pragma mark -
#pragma mark Whether need Navigation Bar Hidden
- (BOOL) needHiddenBarInViewController:(UIViewController *)viewController {
    
    BOOL needHideNaivgaionBar = NO;
    
  // 在这里判断, 哪个 ViewController 需要隐藏导航栏, 如果有第三方的 ViewController 也需要隐藏 NavigationBar, 我们也需要在这里设置.
    if ([viewController isKindOfClass: [MLMineViewController class]] ||
        [viewController isKindOfClass: [MLUserHomePageViewController class]] ||
        [viewController isKindOfClass: [MLLoginViewController class]]) {
        needHideNaivgaionBar = YES;
    }
    
    return needHideNaivgaionBar;
}

#pragma mark - UINaivgationController Delegate
#pragma mark -
#pragma mark Will Show ViewController
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
  // 在 NavigationController 的这个代理方法中, 设置导航栏的隐藏和显示
    [self setNavigationBarHidden: [self needHiddenBarInViewController: viewController]
                        animated: animated];
}

不否认, 这样的写法确实可行, 但是考虑到, 如果某一个第三方的ViewController 同样使用了这种方法来隐藏自己的NavigationBar, 那么就意味着将 NavigationController 的代理指向了其他的ViewController, 那么我们 App 中的所有隐藏NavigationBar 的逻辑就都失效了, 所以为了保险起见, 我将上述代码写到了MLBaseViewController 中, 并在MLBaseViewControllerViewWillAppear 方法中, 将delegate 指向了self. 代码如下:

@implementation MLBaseViewController

#pragma mark - ViewController Life Circle
#pragma mark -
#pragma mark ViewWillAppear
- (void) viewWillAppear:(BOOL)animated {
    [super viewWillAppear: animated];

  // 1. 返回手势代理
  self.navigationController.interactivePopGestureRecognizer.delegate = (id)self;

  // 2. 导航控制器代理
  self.navigationController.delegate = self;
}

#pragma mark - Private Methods
#pragma mark -
#pragma mark Whether need Navigation Bar Hidden
- (BOOL) needHiddenBarInViewController:(UIViewController *)viewController {
    
    BOOL needHideNaivgaionBar = NO;
    
    if ([viewController isKindOfClass: [MLMineViewController class]] ||
        [viewController isKindOfClass: [MLUserHomePageViewController class]] ||
        [viewController isKindOfClass: [MLLoginViewController class]]) {
        needHideNaivgaionBar = YES;
    }
    
    return needHideNaivgaionBar;
}

#pragma mark - UINaivgationController Delegate
#pragma mark -
#pragma mark Will Show ViewController
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
    [self.navigationController setNavigationBarHidden: [self needHiddenBarInViewController: viewController]
                        animated: animated];
}

@end

这样就可以避免, 第三方的ViewControllerNavigationControllerdelegate 指向了一个我们所不了解的位置.

Note1: 子类尽量不要实现 - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated方法, 当然如果你的子类实现了这个方法, 别忘了调用 super 就好了.

Note2: 在ViewWillAppear 方法中, 我们看到了一句代码:self.navigationController.interactivePopGestureRecognizer.delegate = (id)self;, 添加这句代码的原因在于: 当你自定义了NavigationBarBackButton 或 隐藏了NavigationBar之后, 系统的返回手势就失效了, 加上这句之后, 返回手势就 OK 了. 但是这句代码这样写仍然是有潜在问题的, 这会使得用户在使用 App 不当时, App 会产生一种假死的现象, 请看 PopGestureRecognizer Tips.

到这里, 其实我们已经完美的解决了上述的5个 Bug 了, 来看两张动图, 爽一下吧:

几个 UI 不友好的 Bug
NavigationBar 错乱的 Bug

怎么样? 效果是不是还可以? 是不是棒棒哒?



补充内容__20160830


1、一种比较常用的导航栏隐藏显示的方法

还有一种比较常用的NavigationBar 隐藏和显示的方法. 只在ViewController 的基类里的ViewWillAppear 方法中设置NavigationBar 是否隐藏, 而ViewWillDisappear 方法中不设置导航栏是否隐藏. 这样也可以避免一些 UI上不和谐的问题. Note: 如果你遇到了一些切换ViewController 不和谐的效果, 也可以试试这种方法. 代码如下(纯手写代码, 如有拼写错误请见谅):


@implementation MLBaseViewController

#pragma mark - ViewController Life Circle
#pragma mark -
#pragma mark ViewWillAppear
- (void) viewWillAppear:(BOOL)animated {
  [super viewWillAppear: animated];

  // 1. 设置 NavigaionBar 的显示与隐藏, 这种方法只写在这个基类里就可以了, 至于哪些需要隐藏, 哪些不需要隐藏, 修改 -(void) needHideNavigationBar; 方法就 OK 了.
  [self setNavigationBarHidden: [self needHiddenBarInViewController: viewController]
                      animated: animated];
}

#pragma mark - Private Methods
#pragma mark -
#pragma mark Whether need hide navigation bar
- (BOOL) needHideNavigationBar {
  
  BOOL result = NO;

  if ([self isKindOfClass: [MLMineViewController class]] ||
      [self isKindOfClass: [MLUserHomePageViewController class]]) {
    result = YES;
  }

  return result;
}

@end

2、再谈导航栏错乱的 Bug

上文中提到的NavigationBar 错乱Bug 的解决办法, 经过深度测试发现, 当 两个隐藏 NavigationBar 的试图进行切换的时候,仍然存在. 由于项目时间紧迫, 我这里的解决办法是将其中一个页面的 NavigationBar 从原来的CustomNavigationBar 改为了系统的NavigationBar. 问题得到了暂时的解决. 希望对这个问题有研究的朋友可以一起探讨一下. 这个问题在项目不忙的时候, 我也会持续跟进.


Lemon龙说:

如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改

如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您

如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励

如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在简书这个平台能够更快速的成长


上一篇: 🏠
下一篇: PopGestureRecognizer Tips

偶遇疑难杂症
Web note ad 1