这是「iPhone Duo」六支苹果官方 Tech Talks 视频系列的第四篇。前几篇分别讨论设计原则与适配准备,后面的内容还会讲导航栏、多屏和相机。本篇聚焦一个很实际的问题:当屏幕被铰链、摄像头或折叠姿态切开时,现有界面应该移动、缩放,还是换一种布局?

先理解 iPhone Duo 的可用空间

iPhone Duo 有多块显示屏,每块屏幕都有自己的尺寸类。系统还会把铰链,以及内外屏上的摄像头,视为会影响布局的硬件因素。它们形成的区域叫预留区域(reserved region)。

开发时可以把预留区域当成已经存在于布局中的一部分,而不是一块突然出现的黑色空白。系统工具栏和标签栏会在新的安全区域内排列;取景器类界面则要把重要内容和控件放到摄像头区域之外。换句话说,先确保内容确实落在用户能看到、能操作的位置,再考虑怎样填满屏幕。

内容要落在可见、可触达、不被遮挡的位置

内屏折叠后,铰链会把显示屏分成多个区域。跨过铰链放一张照片,看起来就像把照片印在书脊上:图像的一部分会变得难以辨认,内容也不再像一个连续画面。应用界面同样如此,跨越铰链的控件可能失去上下文,触控目标也可能落在不适合操作的位置。

位移:先移动已有元素

对于不能自然绕开预留区域的界面,可以使用位移(displacement)模式。它的做法很直接:根据可用空间调整已有元素的 frame,让重要内容在设备折叠后仍然可见、可访问。

位移的范围不必固定。一个独立按钮可以单独移动;一组相互配合的控件则应该一起移动;如果整个区域的关系都需要保持,也可以移动更大的容器。判断标准是元素之间的功能关系,而不是代码层级。能独立调整的元素不要牵连其他内容;必须共同工作的元素不要拆开。

位移让元素避开铰链,但要留意移动距离

也要控制移动距离。元素离开原来的来源太远,用户就很难把它和原来的内容联系起来。照片选择场景里的上下文菜单就是一个例子:它不会固定在页面底部的正中央,而是随页面移动,并围绕首屏内容对齐。

连续滚动的文章、信息流、文档和列表通常不适合位移。它们本来就能通过滚动适应不同的可用空间,突然在区域之间搬动,会打断阅读的连续性。先判断内容是否已经具备滚动适应能力,再决定是否需要额外处理。

内容该往哪里移动

移动方向由内容的用途决定,最终位置也会随设备姿态变化。

设备像书一样部分折叠时,提示信息等元素可以移动到背面。设备合上后,这些内容会更靠近它原来出现的位置,体验也能继续在外屏上进行。设备立在桌面上时,上方区域适合放需要远距离查看的内容,例如提示信息;下方区域适合放交互控件,例如媒体控制,因为这里更容易形成稳定的触摸表面。

提示信息偏向易读区域,交互控件偏向稳定的触控区域

多个区域都能放置内容时,优先保持上下文关联。搜索功能可以像 iPhone 上一样放在键盘上方。设备打开时,它能利用更多横向空间;设备折叠时,宽度和位置一起调整,仍然保持在正在搜索的视图上方。重要的不是固定某个坐标,而是让控件与它服务的内容保持邻近。

适应新的区域

确定移动方向后,还要处理元素进入新区域时的尺寸与视觉属性。位置和大小是最常见的变化,但间距、排列方式等属性也可能需要调整。

操作表、alert、menu 和 popover 都属于轻量的上下文体验。它们的首要目标是完整可见,系统会自动把这些组件重新定位到预留区域周围。使用系统组件时,先依赖这套行为,不要为每种折叠姿态重写一套定位规则。

系统也会调整已有容器。例如类似「提醒事项」的分屏应用,会改变列宽,将可用空间均匀分成两列,让两列都保持可见。网格则可以在铰链附近增加间距,同时保持外边缘不变,使每个项目留在自己的区域内。这个思路比单纯缩小所有项目更可靠:内容、功能和布局关系都要继续成立。

查询预留区域

需要自己布局时,SwiftUI 可以在 GeometryReader 或 onGeometryChange 修饰符中的 GeometryProxy 上查询预留区域。GeometryProxy 的 reservedRegion 方法返回视图中可用的区域,开发者可以据此决定元素的 frame 和间距。

UIKit 则使用 UIView 提供的 reservedRegion 方法,查询返回区域的 frame 属性,并把它纳入自己的布局计算。两套接口解决的是同一个问题:不要猜铰链或摄像头的位置,直接读取当前视图实际面对的预留区域。

预留区域分为活动和非活动两种。默认情况下,reservedRegion 只返回活动区域;需要检查所有区域时,可以使用 includeInactive 查询选项。折叠产生的分割预留区域只有在用户折叠设备时才处于活动状态。设备平放时,它仍然存在,但处于非活动状态,宽度为零。

用 reservedRegion 查询分割区域

非活动区域也有用途。比如网格布局可以根据“存在分割区域”这个更高层的事实,提前选择偶数列,而不必只在分割区域已经生效后才改变列数。

另一类是遮挡区域(occlusion region)。它不会把区域切成几块,而是遮住其中一小块,像视野内的一个小框。iPhone Duo 上的 FaceTime 通话摄像头就以遮挡区域表示。把遮挡类型传给 reservedRegion,就能查询这类区域;摄像头启用时区域活动,摄像头停用时区域非活动。

系统容器已经会适应

NavigationStack、NavigationSplitView 和 TabView 是系统提供的导航容器,List 和 ScrollView 是内容容器。它们已经包含一部分适应 iPhone Duo 的行为,所以不要一开始就把所有布局改成自定义计算。

如果界面使用了标准容器,先在不同姿态下检查它的实际表现,再补上真正缺失的部分。这样能避免为系统已经处理好的 alert、popover、列宽或滚动行为重复写代码。

Arrangement:导航和内容之间的一层布局容器

有些界面看起来像分屏,却不需要 NavigationSplitView 自带的展开与折叠功能。Arrangement 是为这类场景准备的布局规则。它位于导航容器和内容容器之间,根据一组规则排列两个视图。

Podcasts 的「正在播放」和文字稿很适合说明这个关系。用户点击按钮显示文字稿时,两个视图会分成两半;iPhone Duo 折叠后,分屏布局可以把它们分别放在铰链两侧,让控制装置保持可操作。设备旋转、变成高度大于宽度时,文字稿又以内联形式显示,而不是硬保留分屏。

一个 arrangement 的输入不只是宽度。系统还要考虑水平和垂直尺寸类、视图宽高比,以及是否存在活动的分割区域。它把这些输入转换成布局输出,包括视图是否显示,以及显示时采用什么 frame。

播客播放页和文字稿由 arrangement 分配空间

用 ArrangementView 配置两个视图

SwiftUI 中,可以把 ArrangementView 放进现有的 NavigationStack。它接收 primary 和 secondary 两个视图。以音频笔记应用为例,PlayerView 显示正在播放的音频笔记,UpNextView 显示接下来要播放的笔记。

UIKit 中,UIArrangementViewController 可以作为 UINavigationController 的根视图控制器,再把 Player 和 UpNextView 控制器配置为主要和次要视图控制器。两种实现都把“两个内容如何摆放”交给 arrangement,而不是在每个设备姿态里手动计算。

UIArrangementViewController 作为导航的根控制器

通过 arrangementViewStyle 可以指定首选排列方式。默认样式是 split,也可以显式指定。

split:优先保持清晰的两块内容

split 会把提供的边界分成 primary 和 secondary 两块。默认情况下,视图宽度大于高度时水平分割;高度大于宽度时垂直分割。这套规则同样适用于 iPad、iPhone 和 iPhone Duo 的宽屏比例。

如果应用只接受水平分割,可以使用 split ArrangementStyle 的 axes 方法指定允许的轴。当前视图的主轴如果不能用于分割,ArrangementView 只显示一个视图。比如设备处于竖向姿态、主轴是垂直方向,但配置只允许水平分割时,系统可以只保留 PlayerView。

UIKit 中,对 UIArrangementViewController 调用 update arrangement,并传入与 SwiftUI 中相同轴配置的 UISplitArrangement。

overlay:有前景和背景关系时叠放

overlay 不倾向于把内容并排分开,而是把内容上下叠放。设备折叠时,它又倾向于把 primary 和 secondary 并排放置,让 secondary 获得更多空间。

使用 overlay 时,可以查询 overlayArrangementZIndex 环境属性。这个值会随设备折叠和展开变化,视图可以据此在折叠版和展开版之间切换。UIKit 中,可以通过 UIArrangementViewController 的 view placement 方法查询主视图的 Z 索引,再读取返回状态的 Z index 属性。

overlay 排列方式改变展开与折叠时的叠放关系

如何选择 split 或 overlay

先看应用现有的布局模式。已经用 HStack 或 VStack 做出类似并排布局的地方,优先考虑 split;已经用 ZStack 做出覆盖层的地方,优先考虑 overlay。这些组件的空间关系已经表达了你的意图,也能自然转成相应配置。

如果没有既有模式,再看两个视图的主次关系。前景控件叠在背景内容上方,且背景内容可以滚动,即使偶尔被遮挡也不影响阅读时,overlay 更合适。辅助功能阅读器就是这种情况:控件在前景,可读内容在背景。

如果两个视图都必须清楚可见,并且 secondary 是对 primary 的补充信息,可以考虑 split。播客文字稿提供当前播客的更多细节,播放器和文字稿都不能被遮挡,因此分屏更合适。音频笔记应用里的 UpNextView 同样属于这种关系。

按现有的 HStack、ZStack 模式选择排列方式

ArrangementView 不负责导航

ArrangementView 只负责两个视图的布局,不提供应用导航基础设施。因此,不要把 NavigationSplitView 这样的导航容器放进 ArrangementView。导航层应该留在外面,ArrangementView 处理导航之后的内容关系。

同样,不要把 ArrangementView 放进 List 或 ScrollView 等可滚动容器中。滚动容器自身有内容组织和滚动行为,再套一层会让布局职责混在一起,也不符合这套容器的使用方式。

交付前的检查清单

iPhone Duo 的适配可以从现有界面开始,而不是从零重做:

  • 检查居中布局在铰链和摄像头出现后是否仍然可读、可操作。
  • 对独立控件使用位移;对需要保持关系的一组控件,移动它们共同所在的容器。
  • 让提示信息靠近可见区域,让交互控件落在更稳定的触摸区域;连续滚动内容不要随意位移。
  • 先验证 NavigationStack、NavigationSplitView、TabView、List 和 ScrollView 的系统行为。
  • 对自定义的两栏或覆盖布局,选择 ArrangementView;需要手动避开铰链和摄像头时,再查询 reservedRegion

自适应布局不是把所有内容都移动一遍。只有当移动、调整大小或重新组织能改善体验时才做。先审查居中布局,再判断该用两列、位移还是 arrangement,通常就能找到改动最少、关系保留得最好的方案。

系列文章

这套教程一共六篇,对应苹果官方的六支 Tech Talks 视频: