关于 iphone-duo-dual-pane-patterns
面向 iPhone Duo 双窗格界面开发的实践型设计技能,提供设备尺寸、铰链回调细节,以及窗格内容安排模式。
主要功能
- 01设备尺寸与布局参数
- 02SwiftUI 和 UIKit 铰链回调
- 03将设备姿态映射到布局模式
- 04五种双窗格界面模式
适用场景
- 01确定界面原型和素材尺寸
- 02响应铰链状态变化
- 03为编辑器和对比界面选择窗格模式
- 04在桌面姿态布局媒体与控件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mirzaaghazadeh/ iphone-duo-skills --skill iphone-duo-dual-pane-patterns安装后在 Claude Code 中输入/iphone-duo-dual-pane-patterns确认已加载。
详细介绍
双栏布局模式、尺寸与铰链回调
相关技能分别介绍了平台:../iphone-duo-adaptive-layout/SKILL.md 中的保留区域和布局方式、../iphone-duo-hinge-and-scenes/SKILL.md 中的铰链和场景,以及 ../iphone-duo-design-review/SKILL.md 中的设计原则。本篇是应用层:制作模型或确定素材尺寸时需要的数值、铰链回调的具体形式,以及双屏设计中每个窗格适合放置哪些内容的成熟模式——这些经验取自 Apple 的资料,以及 Microsoft(Surface Duo)、Google(Android 折叠屏)和 Samsung(Flex Mode)十年来的双屏设计指南;它们最终都得出了相同结论。
数值
以下数据来自 Apple 公布的规格。可用于制作模型、规划素材预算和检查合理性,绝不可用作布局常量。布局应在运行时读取尺寸类别、安全区域和保留区域。
| 外屏 | 内屏(展开) | 内屏的每一半 | |
|---|---|---|---|
| 对角线 | 5.4 in | 7.6 in | — |
| 点数(宽 × 高,设备摆放方向) | 466 × 678 纵向 | 951 × 669 横向 | 475.5 × 669 纵向 |
| 像素(宽 × 高,设备摆放方向) | 1398 × 2034 纵向 | 2670 × 1878 横向 | ≈1335 × 1878 纵向 |
| 像素密度 | 460 ppi | 430 ppi | 430 ppi |
| 宽高比 | ≈1.45 : 1(高) | ≈1.42 : 1(宽) | ≈1.41 : 1(高) |
| 尺寸类别 | 紧凑 × 常规(纵向),紧凑 × 紧凑(横向) | 常规 × 常规,任意方向 | — |
Apple 将内屏规格列为“1878-by-2670”;设备展开时宽大于高(164.6 × 117.8 mm),因此自然的展开姿态是横向。
请牢记以下三点:
- 内屏基本上就是横过来的外屏。 两者的宽高比都约为 1.4–1.45 : 1,这就是 Apple 称它们共用一种宽高比的原因。
- 内屏的每一半大致相当于一块外屏。 适合手机屏幕的 UI 可以放进一半,另一半则是额外空间。这就是整个设计理念:一个熟悉的窗格加一个额外窗格,而不是一个拉伸后的手机 UI。
- 展开时,折痕沿长边的中线延伸,因此默认的双窗格布局是左右并排,桌面模式布局则是上下排列。
点数
Apple 的 Tech Specs 只列出像素数据。点数尺寸来自 Apple 的 iOS and iPadOS 27 design resource 中的 iPhone Duo 模板(由社区项目 iPhone Duo Adaptive Design Starter Kit 的 Noah Elhadedy 于 2026 年 9 月 23 日重现,并与 Apple 的模板保持一致):
- 外屏:466 × 678 pt——正好是 1398 × 2034 面板在 3× 下的尺寸。
- 内屏:951 × 669 pt(纵向时为 669 × 951)。这并不是 2670 × 1878 面板的精确 3× 尺寸:系统会以 2853 × 2007 的尺寸渲染(即 App Store Connect 截图尺寸),然后再缩小,类似 iPhone 6 Plus 的处理方式。内屏素材应按点数尺寸的 3× 导出,而不是根据面板像素导出。
- 两种方向下,折痕都位于距边缘 475.5 pt 处,即屏幕中心的 951 pt。
仍请在 Xcode 27.1 模拟器中确认;读取 GeometryProxy.size 或场景边界,不要硬编码这些值。
布局令牌(pt)
用于绘制模型图,以及检查系统在运行时提供的值是否合理。来源列很重要:只有 Apple 值来自 Apple 的设计资源;kit 值是社区入门套件中的设计选择,可随意修改。
安全区域 — 设计上不对称;切勿将一侧的值镜像到另一侧。
| 状态 | 顶部 | 底部 | 前导 | 尾随 | 安全内容尺寸 | 来源 |
|---|---|---|---|---|---|---|
| 外屏纵向 | 0 | 0 | 0 | 84(竖向栏) | 382 × 678 | Apple |
| 外屏横向 | 0 | 0 | 0 | 84,位于摄像头一侧 | 594 × 466 | Apple |
| 内屏横向(平展或书本模式) | 0 | 0 | 0 | 84(竖向栏) | 867 × 669 | Apple |
| 内屏纵向(平展或桌面模式) | 84(状态栏 + 顶栏) | 95(标签栏) | 0 | 0 | 669 × 772 | Apple |
横向栏仅会出现在内屏纵向布局中。其他所有情况下,导航栏、工具栏和标签栏共用一个 84 pt 的竖向栏,从上到下依次为:返回 → 主要操作 → 工具栏组 → 标签栏。
栏和控件
| 元素 | 尺寸 | 来源 |
|---|---|---|
| 竖向栏宽度 | 84 pt | Apple |
| 横向导航栏/工具栏(内屏纵向) | 高 48 pt | Apple |
| 横向标签栏(内屏纵向) | 高 95 pt,内嵌 | Apple |
| 栏按钮/主要栏按钮 | 36 pt/48 pt | Apple |
| 触控目标 | 默认 44 × 44 pt,最小 28 × 28 pt | Apple HIG |
边距、间距、折痕
| 令牌 | 值 | 来源 |
|---|---|---|
| 两块显示屏的布局边距 | 20 pt | Apple |
| 平展时分隔区域的宽度 | 0 pt | Apple |
| 折叠时分隔区域的宽度 | 未公布 — 读取 ReservedRegion.frame | — |
| 折痕避让带(模型图) | 27 pt,以 475.5 pt 为中心 | kit |
| 面板间距 | 外屏 16 pt,内屏 24 pt | kit |
| 正文最大宽度 | 600 pt | kit |
列 — Apple 要求列数为偶数;具体列数为 kit 值。
| 状态 | 列数 | 列间距 |
|---|---|---|
| 外屏纵向 | 4 | 16 |
| 外屏横向 | 6 | 16 |
| 内屏纵向 | 6 | 24 |
| 内屏横向 | 8 | 24 |
| 内屏书本折叠 | 4 + 4,在折痕处相接,折痕上不设列 | 24 |
内屏横向安全区域宽度为 867 pt,以下为其中典型的面板宽度(kit):列表 300–320 + 详情 546–566;侧边栏 280;播放器/主面板约 462(一半)+ 上下文面板约 404。面板内容的宽度应接近外屏尺寸,而不是将其拉伸。
物理规格
| 展开 | 闭合 | |
|---|---|---|
| 宽度 × 高度 | 164.6 × 117.8 mm | 84.1 × 117.8 mm |
| 厚度 | 5.2 mm | 11.3 mm |
| 重量 | 254 g | 254 g |
生物识别使用侧边按钮上的 Touch ID — 不支持 Face ID。任何提及“Face ID”或使用人脸图标的流程,都需要根据 LAContext.biometryType 分支处理。本设备未列出 LiDAR:基于 RoomPlan 或场景深度构建的功能需要提供备用方案。
转轴回调 — 确切形式
均适用于 iOS 27.1(撰写时为 beta),并已对照 Apple 符号文档验证。
SwiftUI
nonisolated func onHingeChange(
isEnabled: Bool = true,
_ action: @escaping (DeviceHingeContext, DeviceHingeContext) -> Void
) -> some View
| 类型 | 成员 | 成员类型 | 备注 |
|---|---|---|---|
DeviceHingeContext | hinge | DeviceHinge? | nil → 设备没有转轴 |
DeviceHinge | angle | Angle | 使用 .degrees/.radians |
DeviceHinge | status | DeviceHinge.Status | |
DeviceHinge.Status | .closed、.partiallyOpen、.fullyOpen | 带静态成员的结构体 | 遵循 Equatable,因此可使用 switch,但需要提供 default |
闭包会接收 (old, new)。使用 isEnabled: 在效果不显示在屏幕上时停止更新,而不是在闭包中忽略更新。
UIKit
init(updateHandler: (UIHingeInteraction, UIHingeInteraction.Update) -> Void)
var isEnabled: Bool
| 类型 | 成员 | 成员类型 | 备注 |
|---|---|---|---|
UIHingeInteraction.Update | hinge | UIHinge? | 视图离开提供铰链更新的层级时为 nil |
UIHinge | angle | CGFloat | 弧度,不是角度 |
UIHinge | status | UIHinge.Status | |
UIHinge.Status | .closed、.partiallyOpen、.fullyOpen、.unknown | enum(Int 原始值) | UIKit 有第四种状态,SwiftUI 没有 |
UIKit 处理程序不是 (old, new) 这样的成对值。铰链发生变化,以及交互在不同层级之间移动时,都会触发它。如果需要计算差值,请自行保存上一个值。
Apple 在参考文档中说明的规则
- 状态由系统根据角度和设备方向决定。 不要根据角度阈值重新推导状态——文档没有规定“低于 N° 即为闭合”。
- 更新速率和精度由系统策略决定,并可能随系统状态变化。不要假定更新频率;应插值或朝最新值进行动画,而不要把每次回调都当作一帧。
- 如果只需要闭合/部分打开/打开状态,请使用
status,而不是angle。 - 角度范围没有文档说明。 不要假定完全展开时恰好为 180°——将
.fullyOpen视为上限,并以此进行归一化。
可复用的归一化器
/// 0 = closed, 1 = fully open. Effects read this; nothing lays out from it.
@Observable final class HingeProgress {
var value: Double = 1 // no hinge → behave as if flat
var isPartial = false
private var maxSeen: Double = .pi
func update(_ hinge: DeviceHinge?) {
guard let hinge else { value = 1; isPartial = false; return }
let radians = hinge.angle.radians
switch hinge.status {
case .closed:
value = 0; isPartial = false
case .fullyOpen:
maxSeen = max(maxSeen, radians)
value = 1; isPartial = false
default: // .partiallyOpen
value = min(max(radians / maxSeen, 0), 1); isPartial = true
}
}
}
// usage
.onHingeChange(isEnabled: effectVisible) { _, new in progress.update(new.hinge) }
UIKit 对应的实现会读取 update.hinge?.angle(已经是弧度),并且必须添加一个 case .unknown:,让上一个值保持不变。
姿势 → 模式
没有“笔记本电脑”或“帐篷”状态。姿势描述的是用户的操作;你的代码能看到的是尺寸类别、保留区域和铰链状态。按以下方式映射:
| 姿势 | 代码观察到的内容 | 模式 |
|---|---|---|
| 闭合 | 外屏、紧凑宽度 | 你已有的手机应用。次要窗格折叠为工作表或推入式页面。 |
| 完全展开 | regular × regular,分隔区域未激活(仅在使用 .includeInactive 时返回) | 两个窗格并排,媒体内容可以跨越中心区域。 |
| 书本式(部分折叠,内屏横向) | 分隔区域已激活且高度大于宽度 | 左右两个窗格。曲面区域内不放置交互元素。 |
| 桌面/笔记本式(部分折叠,内屏纵向) | 分隔区域已激活且宽度大于高度 | 抬起的一半显示内容,平放的一半显示控件。 |
| 帐篷式 | 部分展开,且屏幕通常翻转 | 免手持观看;不要依赖触控。 |
根据区域而非铰链判断折叠方向:
GeometryReader { proxy in
let fold = proxy.reservedRegions(kind: .division).first { $0.isActive }
let isTabletop = fold.map { $0.frame.width > $0.frame.height } ?? false
// …
}
更好的做法是让 ArrangementView 配合 .split 为你选择轴向——宽度大于高度时水平分割,高度大于宽度时垂直分割;这正是书本式与桌面式之间的判断依据。
五种实用模式
Microsoft 的双屏分类法,与 Apple 的容器相结合:
| 模式 | 左侧/上方窗格 | 右侧/下方窗格 | Apple 容器 | 适用场景 |
|---|---|---|---|---|
| 伴随窗格 | 画布(照片、房间、文档) | 工具、图库、属性、提示词 | ArrangementView(.split) | 编辑器、创意工具 |
| 双视图 | 版本 A(原始版本) | 版本 B(结果) | regular 宽度下的两列布局 | 前后对比、差异比较、比较不同变体 |
| 列表-详情 | 列表 | 详情,点击时更新 | NavigationSplitView | 浏览目录、样式、消息 |
| 扩展画布 | 跨越两个窗格的一张图像 | — | 全出血布局 + 用于叠加内容的保留区域 | 地图、全景、视频 |
| 桌面式控件 | 媒体,无按钮 | 摇杆、滑块、操作 | ArrangementView(.split) / .overlay | 免手持观看、展厅、播放 |
根据内容之间的关系来选择模式,而不是根据姿势:伴随窗格 = 一个内容及其工具;双视图 = 同一内容的两个版本;列表-详情 = 多个内容及其中一个内容的详情。
来自四家厂商的六条规则
- 媒体内容跨折痕,控件分开放。 照片和视频可以跨越折痕。 按钮、文本和输入框绝不能跨越折痕。唯一的例外是正在拖动时——手指按住时,被拖动的卡片可以跨越折痕。
- 不要拉伸。 每一半都应呈手机形状。把字体或按钮宽度跨 2670 px 加倍,是典型的错误做法。使用偶数列数,确保没有内容跨越中心线。
- 桌面模式:内容朝上,控件朝下。 翘起的一半用于观看,平放的一半用于触控。折痕附近不要放置任何交互元素。
- 工具放在画布右侧(或底部)——大多数用户的拇指侧,也是 iOS 27.1 已放置垂直栏的位置。
- 两块屏幕 = 对比。 如果应用提供任何前后效果或变体选择,折痕就是天然的分隔线;不再需要单屏滑块。
- 状态在折叠时保留。 打开和合上只会调整场景大小,不会将其拆除。滚动位置、正在输入的文本和当前步骤都应保存在模型中,而不是保存在布局切换时会被丢弃的视图
@State中。对话框应保持在同一半屏内。
由铰链驱动的效果
铰链用于效果和交互,绝不用来控制布局。值得采用的效果:
- 把铰链当作把手。 将归一化进度映射为揭示效果——门扇打开后展现一个场景、掀起封面、推进镜头。用户的手本身就是动画。将其保持为时长不到一秒的叠加层,绝不阻挡输入;当状态为
.fullyOpen时,完成过渡并移除叠加层。 - 合上以保存。 在
.closed时保存数据,并在外屏显示一条简短确认信息。与反向播放揭示效果自然搭配。 - 俯仰视差。 将 CoreMotion 倾斜(偏航/翻滚)与铰链进度(俯仰)结合,移动 2.5D 图层——将深度图拆分为 3–4 层就足够了。限制振幅以避免引发晕动症。
- 连续控件。 任何可以由操纵杆或颤音杆驱动的内容。
始终:
- 处理铰链为
nil的情况(大多数设备没有铰链),并为这些用户提供最终状态。 - 离开
.partiallyOpen时,在else分支中重置状态,避免效果停留在过时的角度。 - 遵循减少动态效果设置——用交叉淡化替代揭示效果。
- 效果不在屏幕上时,传入
isEnabled: false。
购买和付费墙界面
- 表单会自动避开折痕,因此表单付费墙会显示在一半屏幕内。若要跨越两个窗格,请使用自定义双栏布局呈现全屏覆盖层:左侧窗格展示价值主张(最好是用户自己的成果,并带有动画),右侧窗格完整展示套餐和购买按钮。
- 价格、按钮和法律声明必须放在同一窗格内。 绝不要让价格和按钮分处折痕两侧,也绝不要让按钮跨越折痕。
- 在外屏上,改用单栏布局;
SubscriptionStoreView会在那里为你处理试用徽章和续订措辞。 - App Review 3.1.2 仍然适用:计费金额必须是最醒目的元素,试用说明应为次要信息,续订周期必须明确,用户必须能找到恢复购买入口,关闭按钮必须立即可见。
- 折叠不等于关闭。 付费墙显示期间,不要把合上或打开设备视为用户拒绝,也不要据此触发退出优惠逻辑;场景只是调整了大小。
- 由桌面模式触发的付费墙应保持桌面模式的布局:预览在上方,套餐在平放的一半屏幕上。
折叠状态感知的引导流程
- 绝不要求用户打开设备。 邀请用户“打开设备进入”,如果几秒后用户还没有打开,就继续在外屏上显示内容。人们经常会合上 Duo 使用。
- 在任意步骤打开设备,都会将该步骤展开为两个窗格。流程不会重新开始。这是“状态跨折叠保持”原则在引导流程中的应用。
- 先让用户体验到“恍然大悟”的时刻,再请求权限或付费。权限预告屏应紧挨着系统提示,并在用户需要时显示,同时提供“暂不”选项。
- 在付费墙之后请求通知权限,并结合具体承诺(例如“我们会在试用期结束前提醒你”)。
测试矩阵
在 Xcode 27.1 Device Hub 模拟器中,缓慢拖动铰链滑块,逐步经过每种变化,测试每个双窗格屏幕:
| 合上 | 书本式(滑块居中) | 完全展开 | 桌面模式 | 分屏视图,左侧 | 分屏视图,右侧 | |
|---|---|---|---|---|---|---|
| 无黑边 | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| 折叠处没有可点击内容 | — | ☐ | — | ☐ | — | — |
| 状态在转换过程中得以保留 | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
| 铰链效果重置 / 无铰链路径正常工作 | ☐ | ☐ | ☐ | ☐ | — | — |
| 付费墙的价格和按钮位于同一窗格 | ☐ | ☐ | ☐ | ☐ | ☐ | ☐ |
还要在不可折叠模拟器上运行一次,以测试 nil 铰链路径。