mcpField
I
iphone-duo-adaptive-layout

iphone-duo-adaptive-layout

github.com/mirzaaghazadeh设计与 UI250704 次安装今天更新
代码仓库

关于 iphone-duo-adaptive-layout

面向 iPhone Duo 开发者的界面设计技能,讲解如何适配折叠区域、摄像头和设备姿态。内容涵盖保留区域、位移设计及系统布局容器。

主要功能

  1. 01查询分隔区和遮挡区
  2. 02适配折叠状态与设备姿态
  3. 03为控件及关联内容应用位移
  4. 04使用 SwiftUI 和 UIKit 布局方案

适用场景

  1. 01让自定义控件避开折痕
  2. 02设计双窗格和桌面姿态界面
  3. 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、网格、集合视图或分屏视图。
  • 如果需要列展开和折叠行为,应使用真正的分屏视图,而不是排列样式。

实施顺序

  1. 审查居中布局。双栏布局会不会更好?如果不会,适用哪种位移模式?
  2. 优先使用系统容器和呈现方式——大部分适配都能免费实现。
  3. 如果应用使用自定义水平分割或叠加布局,请将其替换为 ArrangementView。这样所有受支持的设备都能获得 iPhone Duo 行为,而不只是这一台设备。
  4. 只有在此之后,才为优先级最高的手动定位控件采用预留区域 API,并手动实现位移。

如需了解铰链驱动的交互而非布局,请参阅 ../iphone-duo-hinge-and-scenes/SKILL.md——实时铰链角度不适合用于视图布局,但适合驱动效果。

评论

支持 Markdown