关于 iphone-duo-adaptive-layout
面向 iPhone Duo 开发者的界面设计技能,讲解如何适配折叠区域、摄像头和设备姿态。内容涵盖保留区域、位移设计及系统布局容器。
主要功能
- 01查询分隔区和遮挡区
- 02适配折叠状态与设备姿态
- 03为控件及关联内容应用位移
- 04使用 SwiftUI 和 UIKit 布局方案
适用场景
- 01让自定义控件避开折痕
- 02设计双窗格和桌面姿态界面
- 03判断何时使用系统自适应容器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mirzaaghazadeh/ iphone-duo-skills --skill iphone-duo-adaptive-layout安装后在 Claude Code 中输入/iphone-duo-adaptive-layout确认已加载。
详细介绍
围绕折痕进行自适应布局
iPhone Duo 部分折叠时,内屏会在中间弯曲。 落在弯曲区域里的内容会更难阅读,也更难点按。书脊上印着照片时也会发生同样的情况:靠近书沟的位置,照片就不再像一张连贯的图像。
因此,折痕并非装饰。它确实会限制内容的放置位置——iOS 会将其建模为一个可供查询并据此进行设计的保留区域。
保留区域
保留区域是硬件施加于布局中的视图区域。 应像处理 iPadOS 上已有的窗口控件那样看待它们:它们并非障碍,只是可用空间的客观情况。
有两种类型,区分它们很重要:
分隔区域会将一个区域划分为多个更小的可用区域。折痕就是分隔区域。内容不会穿过它流动——而是归属于其中一侧。
遮挡区域会覆盖区域的一部分,但不会将其分隔。可以把它们想成位于边界范围内的框架。内侧 FaceTime 摄像头就是一个遮挡区域。
具体来说,iPhone Duo 有三个保留区域,了解它们各自的类型很有必要,因为它们的行为不同:
| 区域 | 类型 | 出现时机 |
|---|---|---|
| 外侧前置摄像头 | 遮挡 | **始终存在。**位于角落,与侧边控件对齐;在 Live Activities 期间会扩展为 Dynamic Island。 |
| 内侧前置摄像头 | 遮挡 | 仅在该摄像头启用时存在。它位于屏幕下方,其他时候不可见;启用时,UI 会移开以显露摄像头。 |
| 折叠区域 | 分隔 | 仅在设备部分折叠时存在,用于分隔内屏并排除中间的弯曲区域。 |
区域可以是活动或非活动状态:
- 折痕的分隔区域仅在设备部分折叠时处于活动状态;设备完全展开时则处于非活动状态。Tech Talk 补充说,此时它的宽度为零;但后续参考资料和 Group Lab Day 3 都指出,非活动区域仍保留一个框架,并建议提前读取它,这样布局就能预先避开折痕,设备弯折时也不会发生移动。依赖其尺寸之前,请先在模拟器中检查该框架。
- 摄像头的遮挡区域仅在该摄像头正在传输画面时处于活动状态。
请注意两种摄像头之间的不对称性:外侧摄像头是布局中的固定因素,而内侧摄像头会在你眼前出现和消失。应将后者视为一种需要通过动画处理的变化,而不是只读取一次的常量。
默认情况下,你只能获取活动区域;Tech Talks 和 API 参考对此说法一致。传入 includeInactive 选项,还可以查看存在但当前未生效的区域——这正适合用于稳定的高层决策。例如,网格可以在任何配备折痕的设备上优先采用偶数列,这样用户每次弯折设备时,布局都不会重新排列。
查询这些区域
在 SwiftUI 中,通过 GeometryReader 或 onGeometryChange 获取 GeometryProxy,
然后向它查询保留区域。请注意,这两个签名有所不同——SwiftUI 比 UIKit 多一个第三个参数:
// SwiftUI, on GeometryProxy
func reservedRegions(
kind: ReservedRegion.Kind,
options: ReservedRegion.QueryOptions,
layoutDirectionBehavior: LayoutDirectionBehavior
) -> [ReservedRegion]
// UIKit, on UIView
func reservedRegions(
kind: UIView.ReservedRegion.Kind,
options: UIView.ReservedRegion.QueryOptions = []
) -> [UIView.ReservedRegion]
在从右向左的布局中,layoutDirectionBehavior 参数很重要:折痕在硬件上的位置是固定的,因此你需要决定区域位置是随内容镜像,还是保持物理坐标,而不能直接沿用默认行为。
在 options 中传入 .includeInactive,也可以获取存在但当前未激活的区域。
每个区域提供的信息不只有矩形:
frame— 区域在视图坐标空间中的位置。isActive(在 Objective-C 中为active)— 区域当前是否处于活动状态。kind— 分割或遮挡。margins— 系统为交互内容保留的间距。参考资料明确指出,frame已包含这些间距,因此只要让控件避开frame就够了;不要再额外按margins内缩一次。将frame按margins内缩,即可得到硬件区域本身——这是全出血媒体的边界,它可以比控件更靠近折痕。id— 在多次查询之间保持稳定,因此你可以区分上次看到的区域和新出现的区域。ReservedRegion遵循Identifiable,因此可以直接用区域驱动ForEach。
坐标框架位于你查询的视图坐标空间中,原点 (0, 0) 在视图的顶部前导角,且只会返回与该视图重叠的区域。遮挡角落的区域可能具有负原点,或超出视图边界,因此不要假设它一定处于边界内。请在负责定位的视图中查询,这样坐标框架与内容才能使用相同的坐标空间。以下两处需要特别注意:
- 在自定义 SwiftUI
Layout中,传给placeSubviews的bounds并不总是从(0, 0)开始。请从零计算位置,并仅在放置每个子视图时加上bounds.origin。 - 在 UIKit 中,于
updateConstraints()中读取区域。UIKit 会跟踪你在此处读取的内容,并在区域发生变化时再次调用该方法,因此你无需自行添加观察者。
在 SwiftUI 中,查询需要第三个参数 layoutDirectionBehavior(默认为 .mirrors),它会为从右向左的布局翻转区域坐标框架,因此自定义 Layout 无需自行处理。传入 .fixed 可获取物理坐标框架。
为自定义、手动布局的控件查询保留区域。如果你使用系统容器,这些情况已经由系统处理——见下文。
位移:设计模式
大多数界面只需围绕保留区域自然流动即可。有些界面则需要更审慎的处理,这种模式称为位移——根据可用空间调整元素的坐标框架,使重要内容在设备折叠时仍保持可见、可触达且不受遮挡。
典型情况是:设备展开时,元素位于中央;折叠后,它会移动到实际适合其用途的区域。
选择合适的范围。 位移的范围可以从单个按钮扩展到整个容器。如果某个元素可以独立存在,就单独移动它。如果多个元素作为一个整体发挥作用,就一起移动——将上下文菜单移离它所属的照片,会破坏两者之间的关联,即使它们在技术上都仍然可见。
不要移动太远。 距离会削弱元素与其来源之间的视觉关联。通常,让两者都靠近折痕对齐,比把其中一个赶到较远的区域更好。
不要位移连续滚动的内容。 文章、信息流、文档和列表本来就可以通过滚动来适应。将它们在不同区域之间重新定位会打断连贯性,却没有任何好处。通常,滚动内容也无需避开折痕;让它延伸到曲线下方没有问题。
内容应该移到哪里
根据用途决定,并注意用途会随设备姿势而变化:
- 像书本一样折叠 — 将提醒等临时内容放到后侧,靠近设备闭合到外屏时它们会继续显示的位置。
- 支在桌面上 — 顶部区域用于放置适合远距离阅读的内容;底部区域是稳定的操作面,因此可点按的控件应放在那里。
- 有多个区域都合适时 — 选择符合上下文的区域。聚焦的搜索栏应留在其搜索的视图上方,并随着设备折叠调整宽度和位置,就像它在 iPhone 上始终位于键盘上方一样。
移动内容后如何调整
位置和大小通常需要调整,但并非只有这两项。分栏视图可以通过重新平衡为均等的 50/50 分栏,让两列始终可见。网格可以保留外边距,只在铰链处加宽间距,让每个单元格保持完整并处于单个区域内。
核心原则:移动、调整大小并重新组织已有内容,绝不移除内容。无论设备以何种姿势握持,每一块内容、每项功能和每种布局都应始终可访问。
自适应容器
在自行编写这些功能之前,先检查系统容器是否已经能实现:
- 导航 —
NavigationStack、NavigationSplitView、TabView - 内容 —
List、ScrollView— 但有一项来自 Apple 参考资料的注意事项: 它们不会在折叠处拆分行,因此一行可能横跨折叠处。 对于连续滚动的内容(见上文的位移),这没问题;如果某一行的控件落在弧形区域内,请读取分区信息并自行布局该行。 - 呈现方式 — 操作表、提醒、菜单和弹出框都会自动避开预留区域并重新定位
排布
导航容器和内容容器之间还有第三种容器: 布局容器,可按规则排列恰好两个视图。这种排列方式在 iOS 27.1 中新增。
该规则是一个函数。输入:水平和垂直尺寸类别、视图的宽高比,以及当前是否有任何分区区域处于活动状态。输出:每个视图是否显示,以及它所获得的框架。
创建布局容器
在 SwiftUI 中,将 ArrangementView 放入 NavigationStack,并为其提供主视图和次视图。在 UIKit 中,将 UIArrangementViewController 用作 UINavigationController 的根控制器,并将视图控制器分配给 .primary 和 .secondary 位置。
分屏样式
这是默认样式。它会在两个视图之间划分可用范围,默认均分,除非另有指定——视图横向较宽时并排显示,主视图在前;视图纵向较高时上下堆叠,主视图在上方。
当只适合沿一个轴布局时,可以对分屏样式应用 .axes(.horizontal) 来限制方向。在 UIKit 中,以相同方式配置 UISplitArrangement,并将其传递给 updateArrangement(_:animated:)。该方法和 setViewController(_:for:animated:) 的 animated 默认值都为 false,因此如果需要动画效果,请传入 true。
如果无法沿某个轴分屏——因为你排除了该轴,或者因为合拢的 iPhone Duo 处于横向状态——系统只会显示主视图,而不会生成不合适的布局。因此,选择主视图时要考虑这一点:它是最终保留下来的视图。
当均分并不合适时,可以调整窗格大小。在 SwiftUI 中,将以下其中一个修饰符添加到视图:splitArrangementLayoutRatio(_:) 用于按容器比例设置大小,splitArrangementLayoutSize(minWidth:idealWidth:maxWidth:…) 用于按点数设置大小,或使用 splitArrangementFixedLayoutSize() 优先采用视图的理想大小。layoutPriority 较高的视图会优先获得所需空间,另一个视图则使用剩余空间。在 UIKit 中,为一个放置项指定 DimensionRange:
var split = UISplitArrangement.split
var primary = split.defaultViewProperties
var width = UISplitArrangement.DimensionRange()
width.minimum = .absolute(250)
width.preferred = .fractional(0.3)
primary.width = width
split.setViewProperties(primary, for: .primary)
arrangementController.updateArrangement(split, animated: true)
此设备上的优势是:关闭次要视图时,主视图不会像在 iPad 上那样重新居中并铺满整个显示屏。它会留在折痕划定的区域内,因此控件仍然触手可及,也不会被弧面挡住。
覆盖样式
分屏倾向于并排显示,而覆盖则倾向于将一个视图叠放在另一个视图上方;设备折叠时,它会改为并排显示——这正是在堆叠视图会变得拥挤时所需要的。
具体来说:在没有有效分区的情况下——设备合拢或完全展开时——主视图会显示在次要视图上方。设备部分折叠后,主视图会移至相对于折痕的尾部或底部区域,次要视图则移至前部或顶部区域。选择哪个视图作为主视图之前,值得先记住这一顺序:主视图最终会位于拇指所在的一侧,而不一定是占据更大区域的视图。
如果默认的侧边不适合你的内容,可以进行设置:在 SwiftUI 中,对视图使用 overlayArrangementEdge(_:);在 UIKit 中,设置覆盖样式视图属性中的 edge。这会决定视图在横向模式下占据哪一侧边缘。
由于视图关系会随设备姿态变化,你通常也需要相应调整从属视图的呈现方式。读取堆叠状态并进行适配:在 SwiftUI 中使用环境值 overlayArrangementZIndex;在 UIKit 中,先在排列视图控制器上调用 state(for:),再读取其 zIndex。堆叠时会折叠的列表,在分配到独立区域后可以展开。
如何选择
沿用应用已有的模式。由 HStack/VStack 构建的内容对应分屏;由 ZStack 构建的内容对应覆盖。两者都能直接转换,并支持 iPhone Duo。
如果没有现成模式可循,可以思考两个视图之间的关系:
- 前景覆盖背景,允许部分遮挡——使用覆盖。控件浮在可滚动的可读内容上方,是最典型的场景。
- 主内容和详情,任何一方都不能被遮挡——使用分屏。播放器和对应字幕是最典型的场景。
不适合使用的情况
- 排列样式不提供导航基础设施。不要在其中放置
NavigationSplitView或TabView——UIKit 中的分屏视图或标签栏控制器也一样。 - 不要将排列样式嵌套在任何会自行布局视图的容器中:
List、ScrollView、网格、集合视图或分屏视图。 - 如果需要列展开和折叠行为,应使用真正的分屏视图,而不是排列样式。
实施顺序
- 审查居中布局。双栏布局会不会更好?如果不会,适用哪种位移模式?
- 优先使用系统容器和呈现方式——大部分适配都能免费实现。
- 如果应用使用自定义水平分割或叠加布局,请将其替换为
ArrangementView。这样所有受支持的设备都能获得 iPhone Duo 行为,而不只是这一台设备。 - 只有在此之后,才为优先级最高的手动定位控件采用预留区域 API,并手动实现位移。
如需了解铰链驱动的交互而非布局,请参阅
../iphone-duo-hinge-and-scenes/SKILL.md——实时铰链角度不适合用于视图布局,但适合驱动效果。