文章介绍在 iOS11 上的一些变化,主要关于 navigation bar、safe area、UIScrollView 的变化和一些适配的方法。

Safe Area

主要关心一个 safeArea (安全区域) 的问题。以下是 Apple 给出安全区域的定义:

The safe area of a view reflects the area not covered by navigation bars, tab bars, toolbars, and other ancestors that obscure a view controller’s view.

简单的来说,安全区域就是不包括 navigtionBar、tabBar、 toolBar 等的区域。我们主要介绍在 iOS11 中 UIView 新增 safeAreaInsets 和 safeAreaLayoutGuide。
在 iOS11 之前是由 topLayoutGuide 和 bottomLayoutGuide 来控制屏幕垂直内容的最大区域。之后,统一成 safeAreaLayoutGuide。

1
var safeAreaLayoutGuide: UILayoutGuide { get }

safeAreaLayoutGuide 表示在试图没有被 bars 遮掩住的部分,它一下几个特点:

  1. UIView 的一个只读属性。
  2. 可以通过设置 view 所在的 UIViewController 的additionalSafeAreaInsets属性来增加或者缩小区域
  3. 子试图的 safeAreaLayoutGuide 和父试图的关系是取交集。也就是说如果子试图完全在父试图内,那么 safeAreaLayoutGuide 的区域和 view 本身一样大。

我们在用 xib 或者 storyboard 的时候就比较清楚的了解 safeAreaLayoutGuide

iOS11 之前的结构,为了防止我们的视图被 NavigtionBar 和 tabBar 覆盖所以我们的布局上下以 TopLayoutGuide 和 BottomLayoutGuide 参考。

iOS11 之后的结构,这里的 Safe Area 就是 safeAreaLayoutGuide, 我们可以看作一个 view ,苹果鼓励我们在这个区域内绘制所有的内容。

以上的 layoutGuide 都是只读的,但是我们可以通过 additionalSafeAreaInsets 属性增加或者减少安全区域的大小。当一个 view 的安全区域发生变化的时候可以通过 viewSafeAreaInsetsDidChange 方法来获取 insets,并且改变安全区域的 insets 会触发 viewDidLayoutSubviews 方法,视图布局会强制刷新。
值得注意的是,在 viewDidLoad 中读取 safeAreaInsets 的值是 0,我们知道在 viewDidLoad 的时候,其实 viewController 的 view 还没还是布局,自然与布局相关的属性还没有值, 所以我们需要在 viewWillLayoutSubviews 或者 viewDidLayoutSubviews 中读取。

UINavigtionBar

navigtion bar 最大的不同就是在 iOS11 上导航栏的图层结构改变了,在 navigtion bar 上集成 UISearchController、大标题。

  • 大标题
    增加大标题的显示,默认是不开启的,可以通过 UINavigationBar.prefersLargeTitles 属性来控制大标题是否显示。

    1
    @property (nonatomic, readwrite, assign) UINavigationItemLargeTitleDisplayMode largeTitleDisplayMode

这是一个枚举类型的属性,分别是 .automatic.always.never 三种类型。

- automatic, 开启自动模式,large title 依赖上一个图层的 item
- always,当前 item 达到顶部的时候总是显示大标题
- never,不显示大标题
  • 集成 UISearchController
    增加了一下两个属性,只需要将 UISearchController 赋值给 navigationItem 就可以实现一个带有 UISearchController 的 navigation bar。hidesSearchBarWhenScrolling 来控制是否随着滚动消失。

    1
    2
    3
    @property (nonatomic, retain, nullable) UISearchController *searchController 

    @property (nonatomic) BOOL hidesSearchBarWhenScrolling
  • navigation bar 的结构
    iOS11 之后的 UINavigtionBar 结构

    iOS11 之前的 UINavigtionBar 结构

    我们可以用 lldb 调试命令来看 UINavigtionBar 的视图结构的变化,在 iOS11 中增加了:

    1. UINavigationBarLargeTitleView 大标题
    2. UISearchBar 搜索框

      从截图中可以看到一个带 _ 的私有属性的变化,在项目中 UINavigtionBar 随着滚动改变透明度,在 iOS11 之后发生 crash 就是因为我们需要设置这些私有属性的同透明度,而这些属性在 iOS11 之后发生了变化,导致强制解包失败。

      iOS11 之后的 UINavigtionBar 视图层级结构

      iOS11 之前的 UINavigationBar 视图层级结构

      含有 LargeTitleView 的 UINavigtionBar 的高度变成了 116 (状态栏20 + 导航栏44 + 大标题52)。当我们没有给 LargeTitleView 赋值,_UINavigationBarLargeTitleView 是添加在 _UINavigationBarContentView 上的;赋值之后,则系统会生成一个 _UITAMICAdaptorView 来承载整个 TitleView。 在 iphoneX 的设备上状态栏变为 44 (其中’刘海’的高度是 24)。

      在 UINavigtionBar 上添加 leftBarButtonItem/rightButtonItem 或者 UISearchBar 会发现这些视图的 frame 或者 size 变得很奇怪。我们需要注意的是 Item 的大小和 frame 避免是 0,需要给视图一个 frame、约束或者重载 intrinsicContentSize 方法。

      在我们在 iOS11 UINavigationBar 结构图中看到在会给 leftBarButtonItem/rightButtonItem 包一层 _UIButtonBarStackView,所以我们设置视图的 frame 会失效,需要用 AutoLayout 来约束这些视图。

在 navigation bar 上增加 item 的注意事项:

  • 避免 item 的视图大小为 0
  • 重载 intrinsicContentSize 方法
  • 用 autolayout 来约束 item

UITableView 和 UICollectionView 的变化

在 iOS11 之前,我们希望 UITableView 全屏幕显示不依赖 navigation bar 和 tab bar 来调整 insets,我们会将 automaticallyAdjustsScrollViewInsets 这个属性设置为 NO,默认是 YES。以下是 Apple Documentation 的描述

A Boolean value that indicates whether the view controller should automatically adjust its scroll view insets.
The default value of this property is YES, which lets container view controllers know that they should adjust the scroll view insets of this view controller’s view to account for screen areas consumed by a status bar, search bar, navigation bar, toolbar, or tab bar. Set this property to NO if your view controller implementation manages its own scroll view inset adjustments.

大致的意思是讲 如果将 automaticallyAdjustsScrollViewInsets 设置为 YES,scrollView 的 insets 会根据屏幕中的 status bar, search bar, navigation bar, toolbar 等来调整。
但是这个属性在 iOS11 之后被遗弃了,取而代之的是 UIViewController 的 contentInsetAdjustmentBehavior

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic, // Similar to .scrollableAxes, but for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable
UIScrollViewContentInsetAdjustmentScrollableAxes, // Edges for scrollable axes are adjusted (i.e., contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentNever, // contentInset is not adjusted
UIScrollViewContentInsetAdjustmentAlways, // contentInset is always adjusted by the scroll view's safeAreaInsets
} API_AVAILABLE(ios(11.0),tvos(11.0));

设置为 UIScrollViewContentInsetAdjustmentNever,表示不调整 contentInset,与 automaticallyAdjustsScrollViewInsets = NO 的效果一样。

UITableView 默认使用 Self-Sizing

项目中的 UITableView 如果没有使用 Self-Sizing 特性,需要将 estimatedRowHeightestimatedSectionFooterHeightestimatedSectionHeaderHeight,这三个预估高度设置为一个 0,则会关闭估算高度的特性。

参考:

https://github.com/stackhou/iOS11-Adaptation