翻译自“View Controller Programming Guide for iOS”。
1 自适应模型
自适应界面可以最大程度利用可用空间。可自适应意味着可以调整内容适配任何iOS设备。iOS的自适应模型支持简单但动态的方式来重新排列内容和调整内容尺寸。利用这个模型,应用程序使用很少代码就能动态适应不同的屏幕尺寸(如图12-1)。
图12-1 适应不同设备和方向
自动布局(Auto Layout)是创建自适应界面的一个重要工具。使用自动布局时,定义规则(称为约束)管理视图控制器视图的布局。可以在界面生成器中可视化创建规则,或者通过代码创建。当父视图的尺寸变化时,iOS根据指定的约束,自动调整其它视图的大小和位置。
自适应模型的另一个重要组成部分是特征(trait)。特征描述了视图控制器和视图必须操作的环境。特征帮助你对界面做高级别的决定。
1.1 特征的角色
单独的约束不足以管理布局时,视图控制器有几个机会做出改变。视图控制器,视图和一些其它对象管理一组特征集,该特征集指定关联该对象的当前环境。表格12-1描述了特征,以及如何使用它们来影响用户界面。
表格12-1 特征
特征 | 例子 | 描述 |
---|---|---|
horizontalSizeClass | UIUserInterfaceSizeClassCompact | 该特征指定界面的整体宽度。用它做粗粒度级别的布局决定,例如视图是否垂直堆放(stacked),并排显示,同时隐藏,或者其它方式显示。 |
verticalSizeClass | UIUserInterfaceSizeClassRegular | 该特征指定界面的整体高度。如果设计要求所有内容没有滚动的适合屏幕,则使用该特征做布局决定。 |
displayScale | 2.0 | 该特征指定内容在视网膜屏(Retina)还是标准分辨率屏幕显示。使用它(如果需要)做像素级别的布局决定,或者显示哪个版本的图片。 |
userInterfaceIdiom | UIUserInterfaceIdiomPhone | 该特征提供向后兼容,并指定应用程序在哪个类型的设备上运行。尽可能避免使用该特征。对于布局决定,使用水平和垂直的size classes代替。 |
使用特征决定如何显示用户界面。在界面生成器中构建界面时,使用特征改变显示的视图和图片,或者应用不同的约束集。许多UIKit类,比如UIImageAsset,使用指定的特征调整它们提供的信息。
下面是一些技巧,帮助理解何时使用不同类型的特征:
- 使用尺寸类(size classes)对界面做粗粒度的改变。尺寸类变化是添加或移除视图,添加或移除子视图控制器,或者改变布局约束的适当时机。也可以什么不做,让界面使用现有的布局约束自动适应。
- 永远不要假设一个尺寸类匹配视图的指定宽度或高度。视图控制器的尺寸类会因为很多原因改变。例如,iPhone上的容器视图控制器可能让其中一个子视图控制器的水平方向为常规,强制它以不同方式显示内容。
- 酌情使用界面生成器为每个尺寸类指定不同的布局约束。使用界面生成器指定约束比自己添加和移除约束更简单。视图控制器自动从故事版中应用合适的约束来处理尺寸类变化。如何为不同尺寸类配置布局约束,请参考“配置故事版处理不同的Size Classes”。
- 避免使用idiom信息决定布局或界面内容。iPad和iPhone上运行的应用程序通常显示同样的信息,应该使用尺寸类决定布局。
1.2 什么时候发生特征和尺寸变化?
特征很少变化,但确实会发生变化。UIKit根据底层环境的变化更新视图控制器的特征。尺寸类特征比显示比例(display scale)更经常变化。idiom特征几乎不发生变化。尺寸类发生变化的原因如下:
- 视图控制器创建的水平或垂直方向的尺寸类改变,通过因为设备旋转。
- 容器视图控制器的水平或垂直方向的尺寸类改变。
- 当前视图控制器的容器显式改变当前视图控制器的水平或垂直方向的尺寸类。
视图控制器层级结构中尺寸类变化会向下传递到所有子视图控制器。作为层级结构中跟对象的窗口对象,为其根视图控制器提供了基准的尺寸类特征。设备方向在竖屏和横屏之间变化时,窗口更新自身的尺寸类信息,并把该信息沿着视图控制器层级结构向下传递。容器视图控制器可以传递未经修改的变化给子视图控制器,或者可以覆写每个子视图控制器的特征。
在iOS 8及以后的版本中,窗口原点总在左上角,设备横竖屏切换时,窗口的bounds会变化。窗口尺寸的变化与相应的特征变化一起沿着视图控制器层级结构向下传递。UIKit调用层级结构中每一个视图控制器的以下方法来报告这些改变:
- willTransitionToTraitCollection:withTransitionCoordinator:方法告诉每个相关的视图控制器,它的特征即将改变。
- viewWillTransitionToSize:withTransitionCoordinator:方法告诉每个相关的视图控制器,它的尺寸即将改变。
- traitCollectionDidChange:告诉每个相关的视图控制器,它的特征已经发生变化。
遍历视图控制器层级结构时,UIKit只在有变化时才报告视图控制器。如果容器视图控制器覆写了子视图控制器的尺寸类,容器的尺寸类变化时,这些子视图控制器不会收到通知。同样的,如果视图控制器的视图有固定的宽度和高度,它不会收到尺寸变化通知。
图12-2展示了选择iPhone 6时,视图控制器的特征和视图尺寸如何更新。从竖屏旋转到横屏时,屏幕的垂直尺寸类从常规变为紧凑。接着,尺寸类变化和相应的视图尺寸变化沿着视图控制器层级结构向下传递。动画改变视图为新尺寸后,UIKit在调用视图控制器的traitCollectionDidChange:方法之前,应用尺寸类和视图尺寸变化。
图12-2 更新视图控制器的特征是视图尺寸
1.3 不同设备的默认尺寸类
每个iOS设备都有默认的尺寸类集,设计界面时,可以作为参考。表格12-2列出了设备在竖屏和横屏时的尺寸类。表格中没有列出的设备与相同屏幕尺寸设备的尺寸类相同。
表格 12-2 不同屏幕尺寸设备的尺寸类
设备 | 竖屏 | 横屏 |
---|---|---|
iPad(所有) iPad Mini |
Vertical size class: Regular Horizontal size class: Regular |
Vertical size class: Regular Horizontal size class: Regular |
iPhone 6 Plus | Vertical size class: Regular Horizontal size class: Compact |
Vertical size class: Compact Horizontal size class: Regular |
iPhone 6 | Vertical size class: Regular Horizontal size class: Compact |
Vertical size class: Compact Horizontal size class: Compact |
iPhone 5s iPhone 5c iPhone 5 |
Vertical size class: Regular Horizontal size class: Compact |
Vertical size class: Compact Horizontal size class: Compact |
iPhone 4s | Vertical size class: Regular Horizontal size class: Compact |
Vertical size class: Compact Horizontal size class: Compact |
重要:永远不要假设应用程序会在特定的size class设备上应用。决定如何配置对象时,总是在该对象的特征集合中检查size class。
2 创建自适应界面
自适应界面应该同时响应特征和尺寸变化。在视图控制器级别,使用特征为显示的内容和内容的布局做粗粒度级别的决定。例如,当尺寸类变化是,可以选择改变视图属性,显示或隐藏视图,或者显示一组完全不同的视图。做出这些大的决策后,使用尺寸变化来调整内容。
2.1 自适应特征变化
特征为不同环境配置应用程序提供了一种方式,并可以使用它们粗粒度的调整界面。大部分特征变化可以直接在故事版文件中完成,有些需要额外的代码。
2.1.1 配置故事版处理不同的尺寸类
界面生成器让界面很容易适应不同的尺寸类。故事版编辑器支持在不同尺寸类配置中显示界面,在特定配置中移除视图,以及指定不同的布局约束。还可以图片资源,为不同尺寸类提供不同的图片。使用这些工具意味着不需要在运行时通过代码做出相同的改变。相反,当前尺寸类变化时,UIKit自动更新界面。
图13-1展示了界面生成器中用来配置界面的工具。尺寸类查看控件(viewing control)改变界面的外观。使用该控件查看界面在给定的尺寸类中的外观。对于单个视图,使用安装控制(installation control)配置该视图在给定的尺寸类配置中是否应该显示。使用复选框左边的加号按钮添加新的配置。
图13-1 为不同尺寸类自定义界面
提示:未安装的视图仍然在视图层级结构中,可以正常操作,但不在屏幕上显示。
图片资产(image asset)是存储应用程序图片资源的推荐方式。每个图片资产包括同一张图片的多个版本,每个版本用于特定配置。除了为标准和视网膜显示屏指定不同的图片,还可以为不同的水平和垂直尺寸类指定不同图片。配置图片资产后,UIImageView对象根据当前尺寸类和分辨率自动选择图片。
图13-2展示了图片资产的属性。改变宽度和高度属性会在目录上为更多图片添加插槽。为每种尺寸类组合填满这些插槽。
图13-2 为不同尺寸类配置图片资产
2.1.2 改变子视图控制器的特征
子视图控制器默认继承父视图控制器的特征。每个子视图控制器与父视图控制器有相同的特征可能没有意义,比如尺寸类。例如,常规环境中的视图控制器可能指定一个或多个子视图控制器为紧凑尺寸类来减少该子视图控制器的空间。实现容器视图控制器时,通过容器视图控制器的setOverrideTraitCollection:forChildViewController:方法修改子视图控制器的特征。
列表13-1展示了如何创建新的特征集,并关联到子视图控制器。只需要在父视图控制器中执行一次这段代码。覆盖的特征属于子视图控制器,直到再次改变它们,或者从视图控制器层级结构中移除子视图控制器。
列表13-1 改变子视图控制器的特征
UITraitCollection* horizTrait = [UITraitCollection
traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
UITraitCollection* vertTrait = [UITraitCollection
traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
UITraitCollection* childTraits = [UITraitCollection
traitCollectionWithTraitsFromCollections:@[horizTrait, vertTrait]];
[self setOverrideTraitCollection:childTraits forChildViewController:self.childViewControllers[0]];
父视图控制器的特征改变时,子视图控制器继承所有父视图控制器没有显式覆盖的特征。例如,当父视图控制器的水平尺寸类从常规变为紧凑时,上面例子中的子视图控制器仍然保持水平方向的常规尺寸类。然而,如果displayScale特征改变,子视图控制器继承新的值。
2.1.3 使Presented视图控制器适应新风格
Presented视图控制器在水平方向为常规和紧凑环境中自动适应。从水平常规过渡到水平紧凑环境时,UIKit默认改变内置弹出风格为UIModalPresentationFullScreen。对于自定义弹出样式,弹出控制器可以决定适应行为,并据此调整弹出。
对于某些应用程序,适应为全屏风格可能会存在问题。例如,通常通过点击弹出框bounds外部来关闭它,但是在紧凑环境中没办法做到,因为弹出框会覆盖整个屏幕,如图13-3所示。当默认适应风格不合适时,可以告诉UIKit使用不同的风格,或者弹出一个更适合全屏风格的,完全不同的视图控制器。
图13-3 常规和紧凑环境中的弹出框
要改变弹出风格的默认行为,需要指定一个代理给关联的弹出控制器。使用presented视图控制器的presentationController属性访问弹出控制器。弹出控制器做任何适应相关的改变前,向代理对象咨询。代理可以返回不同的弹出风格,而不会默认的,并且它可以给弹出控制器提供一个要显示的代替视图控制器。
使用代理的adaptivePresentationStyleForPresentationController:方法指定一个不同与默认的弹出风格。当过渡到紧凑环境中时,仅支持两种全屏风格或UIModalPresentationNone。返回UIModalPresentationNone告诉弹出控制器,忽略紧凑环境,继续使用上一个弹出风格。在弹出框的情况下,忽略改变会在所有设备上提供类似iPad上的弹出框。图13-4并排展示了默认的全屏适应和没有适应。
图13-4 改变presented视图控制器的适应行为
要一起替换视图控制器,实现代理的presentationController:viewControllerForAdaptivePresentationStyle:方法。当适应到紧凑环境时,可以使用该方法在视图层级结构中插入导航控制器,或者加载为小空间设计的视图控制器。
2.1.4 实现自适应弹出框(Popover)的技巧
从水平方向常规变化到水平方向紧凑环境时,弹出框需要额外的修改。水平方向紧凑环境默认将弹出框改为全屏弹出。因为通常通过点击弹出框bounds外部来关闭,所以变为全屏弹出时没法关闭弹出框。可以通过下面其中一种方式关闭弹出框:
- 把弹出框的视图控制器压入现有的导航栈中。如果父导航控制器可用,关闭弹出框,并把它的视图控制器压入导航栈中。
- 全屏弹出时,添加控件来关闭弹出框。可用添加控件到弹出框的视图控制器,更好的选择是使用presentationController:viewControllerForAdaptivePresentationStyle:方法为导航控制器置换出弹出框。使用导航控制器有一个模态界面和控件,可用添加完成按钮或其它控件来关闭内容。
- 使用弹出控制器代理消除任何自适应变化。获得弹出框弹出控制器,并分配一个代理给它,该代理实现adaptivePresentationStyleForPresentationController:方法。该方法返回UIModalPresentationNone,让弹出框继续以弹出框显示。更多信息请参考“使Presented视图控制器适应新风格”。
2.2 响应尺寸变化
尺寸变化的原因有很多,包括以下几点:
- 底层窗口的尺寸改变,通常因为方向变化。
- 父视图控制器调整其中一个子视图控制器的尺寸。
- 弹出控制器改变它的presented视图控制器的尺寸。
发生尺寸改变时,UIKit通过正常的布局过程,自动更新可见的视图控制器的尺寸和位置。如果使用自动布局约束指定视图的尺寸和位置,应用程序自动适应所有尺寸变化,并可以在不同屏幕尺寸上运行。
如果自动布局约束不足以达到想要的效果,可以使用viewWillTransitionToSize:withTransitionCoordinator:方法改变布局。也可以使用该方法创建于尺寸改变动画同时发生的额外动画。例如,界面旋转过程中,可以使用过渡协调器的targetTransform属性为界面的一部分创建反向旋转的矩阵。