对于 navigation bar 的适配,分为以下三种情况:

  1. 使用 UINavigationController 提供的 navigation bar,自定义了 navigationItem
  2. 不使用 UINavigationController 提供的 navigation bar,直接使用 UINavigationBar,自定义后将其加入视图层级中。
  3. 不使用 UINavigationController,也不使用 UINavigationBar,完全使用自定义的 UIView 或其子类。

情况一

使用了 UINavigationController 提供的 navigation bar 这种情况下,UINavigationController 会为我们妥善的处理 safe area 的问题。

需要注意的地方就是,从 iOS 11 开始,navigation bar 上使用了 Auto Layout,所以如果自定义的 titleView 或者 bar button items 的约束或者 intrinsicContentSize 有问题的话,它们的显示就会不太正常,下面是一个例子:

解决方法:正确的设置约束,或者重写 intrinsicContentSize 返回一个正确的值。

建议不要去查找 navigation bar 的 subviews 来设置 frame。

情况二

这种情况也是比较常见的,将 UINavigationController 提供的 navigation bar 隐藏掉(使用 setNavigationBarHidden(_:animated:) 或者 isNavigationBarHidden),再自己配置一个 UINavigationBar 加到视图层级上,达到更好的控制或者实现一些效果。

因为国内大部分 app 都不支持横屏,所以对一个 UINavigationBar 布局的代码很可能会被写成这样:

override func viewDidLoad() {
    super.viewDidLoad()
    let navigationBar = UINavigationBar()
    // configure the navigation bar
    view.addSubview(navigationBar)
    navigationBar.snp.makeConstraints { make in
        make.top.leading.trailing.equalToSuperview()
        make.height.equalTo(64)
    }
}

高度使用的是一个 magic number 约束。这样的写法在不考虑横屏的情况下,在 iOS 11 之前是能正常工作的,但在出现了 iPhone X 之后,这样明显就不对了,甚至这样写在运行 iOS 11 的其他 iPhone 上也不对了,为什么呢?

iOS 10:

iOS 11:

在 iPhone X 会变成什么样可以自行想象。

为了找出问题,先观察一下视图层级。

iOS 10:

iOS 11:

可以发现,在 iOS 10 上,你给 UINavigationBar 设置多高,它的子视图就会有多高,并且内容是从下面开始对齐的。但是在 iOS 11 上,子视图是不会管你设置了多高的,而且它们会从顶部开始对齐,造成了我们所看到的现象。

去调整 UINavigationBar 的高度从一开始就是错误的!

UINavigationBar 它的高度是它自己来决定的,在竖屏下会是 44,在横屏下会自动变为 32。如果从 Xcode 的 Interface Builder 里拖一个 UINavigationBar 出来,你可以看到在竖屏下高度被锁定为 44:

在横屏下高度被锁定为 32:

开启 large title 后,竖屏高度会被锁定为 96:

所以 navigation bar 的约束应该这样添加:

override func viewDidLoad() {
    super.viewDidLoad()
    let navigationBar = UINavigationBar()
    // configure the navigation bar
    view.addSubview(navigationBar)
    navigationBar.snp.makeConstraints { make in
        // iOS 11 and SnapKit 4.0
        make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
        // iOS 10,如果没有升级 SnapKit 的话,可以使用 topLayoutGuide
        make.top.equalTo(self.topLayoutGuide.snp.bottom)
        
        make.leading.trailing.equalToSuperview()
    }
}

但其实在不显示 UINavigationController 的 navigation bar 时,safeAreaLayoutGuide 的顶部或者 topLayoutGuide 是在 status bar 下面的,这样布局会导致 status bar 的颜色和 navigation bar 的颜色不一致(如果不是完全透明的话),我个人的解决办法是下面这样的:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let navigationBar = UINavigationBar()
    // configure the navigation bar
    
    let navigationBarContainerView = UIView()
    // configure the container view
    
    view.addSubview(navigationBarContainerView)
    navigationBarContainerView.addSubview(navigationBar)
    
    navigationBarContainerView.snp.makeConstraints { make in
        make.top.leading.trailing.equalToSuperview()
    }
    
    navigationBar.snp.makeConstraints { make in
        // iOS 11 and SnapKit 4.0
        make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
        // iOS 10,如果没有升级 SnapKit 的话,可以使用 topLayoutGuide
        make.top.equalTo(self.topLayoutGuide.snp.bottom)
        
        make.bottom.leading.trailing.equalToSuperview()
    }
}

这样就能完美的在各个 iPhone 和 iOS 版本上适配,包括横屏,横屏时 UINavigationBar 会自动处理内部内容对齐 safeAreaLayoutGuide,所以其左右是不用对齐到 safeAreaLayoutGuide

不建议去获取 status bar 高度来适配,虽然 iPhone X 的 status bar 高度不会改变,但是其它的 iPhone 的 status bar 高度还是会改变。这样需要引入额外的处理。iOS 11 主界面滑倒最左边的 spotlight 搜索,都没有处理好 status bar 高度变化的这个问题:

情况三

对于完全使用 UIView 或者其子类来实现 navigation bar 的,那当然是根据具体实现加上使用 safeAreaLayoutGuide 或者 topLayoutGuide 布局一下就好了。

Ending

(GUI is hard…

下一篇应该是关于 UIScrollView 的 insets 的适配。