[Diving into WWDC 2017] Updating Your App for iOS 11

为你的 App 适配 iOS 11

活着就是为了改变世界,难道还有其他原因吗? - 史蒂夫·乔布斯

前言

想先看原视频的点这里 iOS 11 为整个生态系统的 UI 元素带来了一种更加大胆、动态的新风格。 下面一起了解一下如何在应用中采用新功能,包括具有集成了搜索的大标题栏,文字图标横向排列的 tab 以及 Table View 带来的惊喜。 通过进一步了解新的模式和增强功能,来为用户提供更加完美的体验。


适配 UIKit’s Bars 带来的新功能

为了完美的用户体验这个终极目标,iOS 和 Android 分别从封闭和开放两个极端不断演化。下图的内容是 iOS 11 增加的一个文件管理相关的新 App,虽然目前功能有限,但有理由相信这又是个乐观的开始。

Files 这个 App 为我们展现了 iOS 11 在 UI 方面的新特性:更大的标题(通过向上滑动,可以让标题回到原来的 UI 效果),整合的搜索框,以及横屏的状态下底部 tab 会呈现下图所示的 icon 和 text 左右并排显示的新效果。

关于 Files 还有更多有趣的新特性,因为不在本文的讨论范围内,就留给大家去探索吧!

UIBarItem

之前 UIBarItem 有个默认值为 nil 的属性 landscapeImagePhone,在 iOS 11 中新增了 largeContentSizeImage 属性,主要用于导入分辨率更高的图片。关于这部分更详细的讨论,可以参考 WWDC2017 Session 215

大标题显示的控制

刚才在 Files 中看到的大标题效果,其实实现起来,非常的简单:

navigationBar.prefersLargeTitles = true  

如果想要针对当前 View Controller 进行个性化的设置,可以通过下面的方式:

navigationItem.largeTitleDisplayMode 

typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {  
    /// 自动模式依赖上一个 item 的特性
    UINavigationItemLargeTitleDisplayModeAutomatic,
    /// 针对当前 item 总是启用大标题特性
    UINavigationItemLargeTitleDisplayModeAlways,
    /// Never 
    UINavigationItemLargeTitleDisplayModeNever,
}

同样是上面提到的 Files 为我们带来了最佳实践,而且你还会在 iOS 11 的多个系统应用中看到 UISearchController 在导航中的集成效果。可喜的是,仅仅通过两个新的属性,实现起来就像下面这样简单:

navigationItem.searchController  
navigationItem.hidesSearchBarWhenScrolling  

UINavigationController 滚动交互

设置了大标题和集成了搜索框的 UINavigationController,在上下滑动的交互中会呈现橡皮筋似得拉伸收缩效果,这种体验用多少细致的语言描述,都不如真实场景一个简单的上下滑动来的简单自然。总结下来会涉及下面三个特性:

  • 搜索框效果更新
  • 大标题效果更新
  • 拉伸收缩的整体动态变化效果

UIToolbar and UINavigationBar — Layout

为了上面提到的特性实现起来更加自然而优雅,在 iOS 11 中,针对 UIToolbar 和 UINavigaBar 也做了新的自动布局扩展支持(记得当时现场此处想起了掌声)!这里需要格外注意下,对于 UINavigationBar 这些处于醒目位置而且又在整个容器中长久有效的设置,一定要格外关注,正如苹果的工程师所说:我们假定你知道自己在做什么…… 其中最为重要的一点是使用普通视图时避免大小为 0 的情况,对此可以从以下几方面解释:

  1. UINavigationBar 和 UIToolbar 负责提供位置
  2. 开发者则必须提供视图的大小,这里又包括:
    • 对宽度和高度的约束;
    • 实现 intrinsicContentSize;
    • 通过约束关联你的子视图;

Margins and Insets

layoutMargins

不管你是不是 AutoLayout 的拥簇者,在 iOS 8 添加的一系列 layoutMargins 相关的属性和方法,想必都不陌生,可喜的是在 iOS 11 中这些将更为强大和优雅。

先通过下图带大家一起回忆下 layoutMargins,这是 UIView 中的 UIEdgeInsets 结构体类型的属性,通过它你可以设置目标的内边距(top, left, bottom, right)。试想这样一种场景:在 LTR 的排版格式下,设置 right = 30,变换到 RTL 的排版格式后,坑就出现了,因为这时 left = 30,才不会显得效果突兀。在 iOS 11 中,通过新引入的 directionalLayoutMargins 属性可以完美解决这个问题。

这是一个 NSDirectionalEdgeInsets 结构体类型的属性,从下面的对比可以看出,leading 和 trailing 取代了之前的 left 和 right。

typedef struct UIEdgeInsets {  
    CGFloat top, left, bottom, right;
} UIEdgeInsets;

typedef struct NSDirectionalEdgeInsets {  
    CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));

这时对上面提到的场景下,通过设置 trailing = 30,就会针对不同的排版格式自动去同步 left 和 right。(此刻现场又想起了掌声)

变化不止于此,以往当你添加 View 到 View Controller 时,UI Kit 会根据内置的特定值对 layoutMargins 微调,遗憾的是这些调整对外是封闭的。没错,iOS 11 有了新变化(此处可以提前鼓掌),在 UIViewController 中同样是刚刚提到的 NSDirectionalEdgeInsets 结构体类型的 新属性 systemMinimumLayoutMargins 来描述通过硬件信息计算出来的合理的系统最小内边距,这个值无法修改。但是否遵循这个最小内边距,可以通过属性 viewRespectsSystemMinimumLayoutMargins 来控制。禁用后,你依然可以通过上面提到的 directionalLayoutMargins 来依据你的特定场景或者异于系统的独特审美来改变内边距。比如下图这个样子:

安全区域(Safe Area)

安全区域这个词是用来描述不会被上层视图遮挡的显示区域。其实这个区域的边界位置并不安全,想必都遇到过导航栏遮住滚动视图的情况。iOS 7 开始,在 UIViewController中引入的 topLayoutGuide 和 bottomLayoutGuide 在 iOS 11 中被废弃了!取而代之的就是安全区域的概念,提供了两种方式来获取安全区域的参照值:safeAreaInsets 和 safeAreaLayoutGuide,即 insets 或者 layout guide。 下面以系统的照片应用为例,来说明动态改变安全区域,以后做起来是多么的优雅:

在照片应用的交互中,点击某张照片进入的大图预览模式,一般底部会出现相邻照片的预览工具栏,如何才能让多个场景的变化中底部的工具栏不会遮挡住屏幕中央的照片,在 iOS 11 中你可以像下面这样做:

// 动态的改变 安全区域
UIViewController.additionalSafeAreaInsets  
UIView.safeAreaInsetsDidChange()  
UIViewController.viewSafeAreaInsetsDidChange()  

UIScrollView 和 UITableView 的新特性

Scroll View

以下是 UIScrollView 在 iOS 11 新加入的属性和方法,主要是对上面提到的安全区域和对 layout guide 的更多支持。 值得注意的 adjustedContentInset 属性替代了 contenInset 所描述的区域并且整合了 UIView 的安全区域概念。

typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {  
    UIScrollViewContentInsetAdjustmentAutomatic, 
    UIScrollViewContentInsetAdjustmentScrollableAxes,
    UIScrollViewContentInsetAdjustmentNever,
    UIScrollViewContentInsetAdjustmentAlways,
}

// 通过上面的枚举,配置 adjustedContentInset 的行为
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;

// 如果 contentInsetAdjustmentBehavior 允许,此属性可以整合 safeAreaInsets 来描述安全区域
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset;

// 改变 adjustedContentInset 的 response 和 delegate
- (void)adjustedContentInsetDidChange;
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView;

// 用于描述未经转换的 content area 区域
@property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide;
// 用于描述未经转换的 scroll view 的 frame
@property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide;

之前的 contentInset 并没有废弃,下图可以给出更加直观的解释:

Table Views

除了继承了父类的以上变化之外,对于 UITableView 本身,也进行了很大的调整,主要包括以下几方面:

默认启用 Self-Sizing

或许这应该是 UITableView 最大的改变:自从 iOS 8 引入 Self-Sizing 以后,动态内容的展示又多了一种优雅的实现方式。而在 iOS 11 中,Self-Sizing 默认启用,这就意味着,你需要确保所有的 cells 都设置好了内部约束,以便告诉 Table View 它应该有的大小,或者去实现相应的 delegate。 但是,你可能并不想在你的 App 中写过多的系统差异的代码,而只是为了过早的体验 iOS 11 的这种新特性,那么你可以通过下面的方式去禁用 Self-Sizing:

override func viewDidLoad() {  
    tableView.estimatedRowHeight = 0 
    tableView.estimatedSectionHeaderHeight = 0 
    tableView.estimatedSectionFooterHeight = 0
}

separatorInset 扩展

iOS 7 引入 separatorInset 属性,用以设置 cell 的分割线边距,在 iOS 11 中对其进行了扩展。可以通过新增的 UITableViewSeparatorInsetReference 枚举类型的 separatorInsetReference 属性来设置 separatorInset 属性的参照值。

typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {  
    // 设置为 separatorInset 属性的值被解释为单元格边缘的偏移量。
    UITableViewSeparatorInsetFromCellEdges,
    // 设置为 separatorInset 属性的值被解释为分割线的偏移量。
    UITableViewSeparatorInsetFromAutomaticInsets
}

Table Views 和 安全区域

关于 Table Views 特有的安全区域相关的特性,主要注意以下的三点:

  1. separatorInset 被自动地关联到 safe area insets,这样使得 Table Views 免受 View Controller 的影响;
  2. UITableviewCell 和 UITableViewHeaderFooterView 的 content view 在安全区域内;因此你应该始终在 content view 中使用 add-subviews 操作。
  3. UITableViewHeaderFooterView 可以在所有的 headers 和 footers 中使用,包括 table headers 和 footers、section headers 和 footers;同样的也应该始终使用 add-subviews 操作。

滑动操作(Swipe Actions)

这是个让人兴奋的新特性,也许你之前在系统的邮件应用中尝试过丰富的左划和右划操作。没错,iOS 11 中这些手势操作,通通都可以在你的 App 中实现(此处有掌声)。

为了实现上图展示的丰富效果,你需要从下面三个方面去适配新的 API:

  • Images - 可以添加图片了
  • 设置 Leading 和 Trailing 操作
  • 完成的处理和取消的处理

最后

以上就是本节(Session 204)的主要内容了,对我个人而言,导航栏更醒目的标题的新 UI 风格,还需要再适应下,但至少搜索框的整合,给我们提供了更多的方便。本节大量篇幅涉及到的安全区域概念,或许会在以后的开发中,特别是使用 Autolayout 的童鞋们,产生深远的影响。好啦,我已经迫不及待要在自己的 App 中实现惊艳的滑动效果啦~


【参考及扩展阅读】