如何优雅的使用 iOS 导航栏

iOS 导航栏一个被翻来覆去说的话题,本来是不想写这篇文章的,可是最近在升级工程导航栏的时候,还是有一些自己的感悟。虽说是老生常谈吧,但是一番操作下来,也遇到了一些问题和想到了一些优雅的解决方案,而且这些问题之前确实少被记录或者说没有统一的记录汇总。这儿我会把我这次遇到的问题,以及 iOS 开发中导航栏通用的解决方案都会做一下介绍。在开始之前,我们先看一下 iOS 导航栏在一些 App 之间的表现,从而引出这次讨论的话题。

一些导航栏实例

有问题的 iOS 导航栏实例

优雅的 iOS 导航栏实例

下面一一来说下上面这些 App 的导航栏:

  • QQ:QQ 这张截图代表的问题是两个页面导航样式不同,比如:前一个页面的导航栏透明,后一个页面的导航栏不透明; 又或者两个页面导航栏颜色不同,侧滑的时候如果没有处理好,都是会出现问题的。类似的问题还存在于现在的很多 App 中,比如:支付宝...
  • 美团:美团这张截图代表的问题是侧滑返回的时候,上面页面侧边有一个渐变阴影,但是仔细观察就发现这个侧滑阴影到导航栏这儿就突然没有了,又因为下面页面的导航栏是透明的,所以这儿看起来就非常不协调。类似的问题还存在于现在的很多 App 中,比如:豆瓣...
  • 钉钉:钉钉这张截图代表的问题是返回按钮设置的太靠近左边,导致返回的时候按钮会被切掉一部分。ps:目前钉钉已经解决了这个问题,但是我没有找到可以代表这类问题的 App ,所以这儿暂时委屈下钉钉了。另外这张截图我也是从别人文章中扣出来的,侵删
  • 小豆苗:小豆苗这张截图是比较完美的解决导航栏问题的实例。在前一个页面透明,后一个页面不透明的情况下,侧滑返回导航栏样式没有问题,并且也没有出现美团类似的问题。唯一的问题就是缺少了侧滑时候导航栏上面一些元素的渐隐效果。ps:这个 App 相对于其他 App 比较小众,之所以选择这个 App 是因为这个是我维护过的一个 App 可以完整的说出技术实践方案,这儿暂且不表,后面在谈。
  • 今日头条:今日头条这张截图也是比较完美的解决导航栏问题的实例。和小豆苗相比他在返回的时候,额外在下面页面加了一个缩放的动画,来弥补缺少侧滑时候导航栏上面一些元素的渐隐效果。类似的 App 有汽车之家...
  • 微信:微信这张截图不用多说,就是 iOS 导航栏的最佳实践,不仅保留了系统侧滑时候导航栏上面按钮的渐变动画,而且也不存在上述说的一系列问题。

QQ 类似问题的解决方案

QQ 这类导航栏一般实现的代码是这样的:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    // 设置当前页面导航栏的样式
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    // 恢复默认导航栏的样式
}

这样写起来是很简单,但是 iOS 的导航栏是整个 UINavigationControllerviewControllers 共用一个 UINavigationBar 所以这样设置在页面切换的时候,就有可能出现上述问题,整体的解决方案分为两个方向。

方案一:为每个 UIViewController 单独创建一个自己的导航栏

这种方案的实现方式是,每个页面都隐藏系统的导航栏,然后自己添加一个 UIView 或者 UINavigationBar 来充当看到的导航栏。这样的好处就是实现一次之后,后期几乎不会有什么问题。缺点就是,对于已有工程改动起来麻烦,而且这种方式也会丢失侧滑的时候导航栏上面元素的一些渐隐动画,小豆苗正式采用了这种方案。当然为了弥补这个缺点,可以去实现一个类似今日头条那样的转场。或者自己去实现类似系统那样的渐隐动画。

方案二:侧滑的时候添加假导航

在侧滑的时候,检测两个页面之间的导航栏的样式是否相同,如果不同,则将系统的导航隐藏或者设置为透明,然后在两个 UIViewController 之上添加对应的假导航来做过渡动效。侧滑完成之后将导航设置回要显示页面导航的样式,并且移除假导航。这个方案的缺点就是需要自己去实现这个效果,当然也可以考虑一些第三方库比如:YPNavigationBarTransition ,优点就是可以完美的实现导航栏的切换,并且对于已有工程代码改动并不会很大,仅仅限于基类的 UINavigationController

美团类似问题的解决方案

美团这类问题出现的原因是,侧滑的时候下面页面导航透明,上面页面导航不透明,导致上面页面 viewy 值并不是从最上边屏幕开始的,而是从导航部分开始的。所以在侧滑的时候,系统做过渡动效添加的阴影线也是从导航部分开始的,就导致看到的阴影线到导航这儿戛然而止。看下图可以更直观的看出这一原因,图中选中的 UIImageView 就是侧滑时候出现的阴影线。

美团这类问题的原因

解决方案

这个问题的解决方案要从 UIViewControlleredgesForExtendedLayout 这个属性说起。这个属性默认为 UIRectEdgeAll,也就是说一般情况下 UIViewController view的大小是全屏幕尺寸的。但是如果你设置这个属性为 UIRectEdgeNoneUIViewController view 的大小就会自动除去导航栏的部分,这样会方便我们布局。但是同样会造成侧滑返回时候的阴影问题。还有一种情况就是,当设置导航栏不透明时候, UIViewController view 的大小也会自动除去导航栏的部分,这个时候系统提供了另一个属性 extendedLayoutIncludesOpaqueBars 来解决这个问题,当 extendedLayoutIncludesOpaqueBars = YES 即使导航栏不透明 UIViewController view 的大小还是全尺寸的,这样侧滑的时候,阴影线就会是整个屏幕高度的,但是我们在写页面布局的时候,就需要注意一下。

在解决这个问题的时候,开始我并没有想用上面的方法,而是想着在侧滑的时候,去获取系统做阴影的 UIImageView 然后去改变这个UIImageViewframe 从而解决该问题。但是我并没有找到一个手段可以获取到对应的这个 UIImageView 从而使我放弃了该方法,如果你有方式获取到这个视图不妨一起来探讨,直接修改视图的这个方案的好处在于不用改动现有工程的代码。对于已有的工程,修改 extendedLayoutIncludesOpaqueBars 属性还是有不少的工作量,我想这也是很多 App 没有修改这个问题的原因之一。

钉钉类似问题的解决方案

钉钉这类问题出现的原因是因为一般我们都是通过设置导航的 leftBarButtonItem 来充当返回按钮。但是在 iOS11 的时候,苹果对导航栏进行了改版设置的 leftBarButtonItem 会被包装进一个叫 _UIButtonBarStackView 中。而这个 View 与屏幕左边的尺寸固定为 16,但是一般我们做 App 的时候,设计又会特别的将返回按钮设置的特别靠左,基本会小于 16,这时候我们一般是通过调整 UIButtoncontentEdgeInsets 调整出这样的效果,这样在正常显示的时候没有问题,但是在侧滑返回的时候,就会出现上述钉钉这样类似的情况。针对这个问题解决方案就是找到 _UIButtonBarStackView 将它的 x 改为从 0 即可。当然侧滑返回的时候,如果 rightBarButtonItem 设置的太靠右也会出现这样的问题,解决方法是一样的。这里给出一个我的解决方案,代码如下:

@interface LeftContainerView : UIView

- (instancetype)initWithCustomView:(UIView *)customView;

@end

- (instancetype)initWithCustomView:(UIView *)customView {
    if (self = [super initWithFrame:customView.bounds]) {
        [self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
        [self addSubview:customView];
    }
    
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    [self removeObserver:self forKeyPath:@"frame"];
    if ([keyPath isEqualToString:@"frame"]) {
        if ([self.superview.superview isKindOfClass:NSClassFromString(@"_UIButtonBarStackView")]) {
            self.superview.superview.transform = CGAffineTransformMakeTranslation(-16, 0);
        } else {
            self.frame = self.bounds;
        }
    }
    [self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"frame"];
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(0, 0, 100, 44);
    button.backgroundColor = [UIColor redColor];
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:[[LeftContainerView alloc] initWithCustomView:button]];    
}

这里通过自定义 LeftContainerView 然后在设置 leftBarButtonItem 的时候都使用这个作为 CustomView,这里面已经纠正了偏移的值。当然除了这个方案,还有其他的一些调整 _UIButtonBarStackView 的方案,或者可以和公司设计商量返回按钮不要往左边偏移太多。

自己工程中特有的问题

公司 App 的导航栏还处于 iOS6 升级 iOS7 做的一次改造,方案用的是截图的方案。因为截图会带来一些性能损耗并且这次在做整个 App 大版本升级的时候也出现了一些问题,所以就考虑整体优化下这一块。经过一系列调研,最终我是选择了使用侧滑的时候添加假的导航这种方式来做这次的改版升级,真假导航这块的处理是在导航栏代理方法 willShowdidShow 中去处理,这样工程中本类就有一个基类 UINavigationController 处理起来成本是最小的。在修改导航栏的时候,上面说的问题都有遇到,类似钉钉这样的问题,工程里面的 leftBarButtonItemrightBarButtonItem 本来也是统一处理的,所以改动成本也很小,但是类似美团这样的问题,改动起来就很大了,而我也仅是选取了部分页面去做修改。

另外我们公司的 App 还有一个很致命的问题,就是有着整个 App 级别的新旧切换,这样我在使用新版的时候还不能去动旧版的方式,当然新版的做法旧版所有页面也是完全支持的,但是里面有很多的问题已经不值得在去处理了。关于导航栏新旧兼容,我用了一个很投巧的方式,是在 UINavigationController 里面直接做了处理,针对新旧版,使用不同的处理方式,这样确实省了很多不必要的麻烦。

感悟

其实导航栏的问题并不难,但是纵观市场上的 App 导航栏都有着这样或者那样的问题。我想一来是因为苹果导航栏的 Api 一直在变化,导致大家适配起来都有一定的成本,而且没有出太多的问题,就先不去动它;二来就是开发者缺少一份打造完美导航栏的心态。另外本文也只是记录了自己在做的时候遇到的一些问题,并没有去详细讲解导航栏相关的基本知识,当然还有一些我没有碰到的问题,也欢迎大家在评论区留言一起探讨。

推荐阅读更多精彩内容

  • 1. 今天坐在星巴克里,看到有一家人是这样互动的,年轻的妈妈抱着5个月大的宝宝,外公外婆也坐在旁边。妈妈把咖啡给了...
    Vidya程莹阅读 1,183评论 4 52
  • 我的名字叫蒂娜·格兰杰。我的头发是金色的,眼睛是翠绿的,很有神。我是一个十足的哈迷,很想去霍格沃茨上学。就在...
    蒂娜格兰杰阅读 187评论 3 4
  • 一开始读完这本书,第一个感觉就是质疑,觉得一个人大概不可能像书中所描写的福贵那样活着吧。从吃喝嫖赌的少爷活成命途多...
    李睿_阅读 210评论 0 0