让应用为 iPhone Duo 做好准备:一个完整的实例
如何让应用为 iPhone Duo 做好准备? 用 iOS 27.1 SDK 构建,在模拟器里把每种姿态都走一遍,修好走查中发现的问题,再围绕 Apple 尚未公布的两个日期安排提交。iPhone Duo 将于 10 月 23 日搭载 iOS 27.1 发售。1 应用在它上面能得到什么,取决于二进制文件里标记的 SDK 版本:同一个源文件分别以 26.0、27.0、27.1 链接,启动后依次是一个手机大小的方框、状态侧轨旁边的一个窗口,以及整块屏幕(栏沿侧边排列,折痕也会被报告)。12 截至 10 月 2 日,只有 Apple 的 beta 能标记 27.1 或更高版本(带 Duo 模拟器的 Xcode 27.1 beta,以及 Xcode 27.2 beta),而 App Store Connect 只接受它们的构建用于 TestFlight,不能上架商店。8 本文以 Kiradex 为例把整件事做完。这是我们正在 TestFlight 上测试的一款卡牌收藏应用:哪些东西是白得的,走查发现了哪些读代码时没发现的问题,两块屏的截图怎么做,以及一份可以直接交给编程智能体的任务说明。
TL;DR
- SDK 标记就是开关。 iOS 读取二进制文件
LC_BUILD_VERSION中的 SDK 版本。标记为 27.1 的探针拿到了整块屏幕:合上时 466 × 678 点,展开时 951 × 669 点,并且有竖向栏和保留区域。标记为 27.0 的探针在状态侧轨所在的边缘前 80 点处止步,栏保持横向,也得不到任何关于折痕的信息。标记为 26.0 的探针则是装在方框里的一台 375 × 667 手机。请用otool -l检查您自己的构建。12 - 日历上还有两个未定的日期。 TestFlight 自 9 月 18 日起接受 27.1 SDK 构建,自 9 月 16 日起接受 27.2 beta 构建;App Store 只接受 27.0 SDK 构建;Duo 截图尺寸已经公布,而上传功能“will be available later this year”(将于今年晚些时候提供)。89 上传时现在必须包含启动屏幕键。10
- 大部分工作由系统容器完成。 一个
TabView、五个标签页中四个里的NavigationStack,以及同时带标题和符号的工具栏项目,就让 Kiradex 的栏移到了侧边,不需要任何 Duo 专用代码。一个ArrangementView把展开的屏幕一分为二,并在 Book 姿态下把分隔线移到折痕上。一个onHingeChange在手机展开时重放应用的开场动画。13 - 走查发现了读代码时没发现的问题。 只有一个文字“Done”按钮的表单会预留侧轨却让它空着。全屏卡牌会伸到外屏摄像头下面。转成竖屏后,分栏会上下堆叠。跨越折痕带过来的卡牌会落进被拉伸的 compact 布局。为内屏编写的 regular 宽度布局,在横放的 6.9 英寸 iPhone 上会走样。13
- 在商店接受 27.1 之前,一套源代码需要两个 Xcode。
#available帮不上忙;27.0 SDK 里根本没有可供编译的ArrangementView。用一个编译条件标志,或者#if canImport(SwiftUI, _version: 8.0.85),把新调用隔离起来。14 - 截图也出自同一次运行。 模拟器的截图恰好是 App Store Connect 要求的尺寸,Apple 的 Duo 边框素材的开口也是这个尺寸。Apple 的规则依然要求正面、不加修改,不得对设备进行 3D 渲染。911
10 月 2 日的现状
| 状态 | 日期 | |
|---|---|---|
| 手机 | 10 月 16 日开始预购,10 月 23 日发售,“available with iOS 27.1”(随 iOS 27.1 提供)1 | 9 月 9 日 |
| SDK | Xcode 27.1 beta(27A9269)包含 iOS 27.1 SDK 和 iPhone Duo 模拟器;Apple 的 Releases 信息源中没有第二个 27.1 beta,也没有候选发布版。Xcode 27.2 beta 包含相同的 API,但没有 Duo 模拟器814 | 9 月 18 日;信息源于 10 月 2 日查看 |
| TestFlight | 接受 Xcode 27.1 beta 和 Xcode 27.2 beta 的构建,内部与外部测试均可8 | 9 月 16 日、18 日和 28 日 |
| App Store | 接受 Xcode 27 和 27.0 SDK 的构建。没有任何条目向 27.1 SDK 开放8 | 9 月 14 日 |
| Duo 截图 | 尺寸已公布;上传“will be available later this year”(将于今年晚些时候提供)9 | 9 月 9 日 |
| 启动屏幕 | 使用 iOS 27 SDK 或更高版本构建的应用,上传时必须提供10 | 技术说明于 9 月 14 日修订 |
其中三行在等 Apple,但没有一行会阻碍工作。从表中得出的顺序是:先让应用用上 27.1 SDK 并装进模拟器,把每种姿态走一遍,修好走查发现的问题,把这个构建放进 TestFlight;商店提交和 Duo 截图则各自准备就绪,等待各自开放的那一天。
Apple 自己的起点是六场 Tech Talk 中的第一场,时长十分钟,下文默认您已经看过:
第一步:SDK 标记决定手机给您什么
这场讲座以分三档的兼容性承诺开场。未使用 iOS 27 SDK 构建的应用照样能运行:“When the device is closed, your app will use the screen space to the left of the status bar and camera. When the device is open, your app will be a familiar size and aspect ratio.”(设备合上时,应用使用状态栏和摄像头左侧的屏幕空间;设备展开时,应用保持熟悉的尺寸和宽高比)(0:36)采用 iOS 27 SDK 的应用“will extend to the left of the status bar area on the inner display.”(会在内屏上延伸到状态栏区域的左侧)(0:58)接着是:“When you build your app with the iOS 27.1 SDK, your app extends to the edge of the screen. Standard navigation and toolbar buttons now lay out vertically under the status bar.”(用 iOS 27.1 SDK 构建时,应用延伸到屏幕边缘,标准导航按钮和工具栏按钮改为在状态栏下方竖向排列)(1:05)3
这三档并不取决于您的代码,而取决于二进制文件里的一个数字:链接器写进 LC_BUILD_VERSION 加载命令的 SDK 版本,iOS 在启动时读取它。为了看清这个数字有多大分量,我做了一个小探针:一个 TabView,里面是带五项工具栏的 NavigationStack 和一个 ArrangementView。我用同一个源文件构建了三次,三个二进制文件唯一的区别就是这个数字:26.0、27.0、27.1。然后把三个都放到 iPhone Duo 模拟器上,逐一跑遍每种姿态。12

一个源文件,三种 SDK 标记,展开且竖直放置。从左到右:26.0、27.0、27.1。
| 姿态 | 标记 26.0 | 标记 27.0 | 标记 27.1 |
|---|---|---|---|
| Closed | 375 × 667,缩放后放进侧轨旁边的空间 | 386 × 678,位于侧轨旁边 | 466 × 678,整块屏幕 |
| Open | 375 × 667,屏幕正中的一台手机 | 871 × 669 | 951 × 669 |
| Open,旋转 | 375 × 667 | 669 × 871 | 669 × 951 |
| Closed,旋转 | 667 × 375 | 678 × 386 | 678 × 466 |
| 宽度尺寸类别,Open | compact | regular | regular |
| 栏 | 全部横向 | 全部横向 | 竖向,展开并旋转时除外 |
toolbarVerticalEdge |
nil | nil | trailing,展开并旋转时为 nil |
| 保留区域 | 无 | 无 | 折痕、两个摄像头、状态列 |
| 半折 | 无变化 | 无变化 | 折痕变为活动的 division;split 形式的 arrangement 在折痕处留出空隙 |
窗口尺寸以点为单位,均为各构建自身窗口报告的数值。12
中间那一列请读两遍,因为大多数应用即将发布的正是它。用 Xcode 27 构建的应用在内屏上能得到 regular 宽度和大部分屏幕,比第一列那种“方框里的手机”确实进步了不少。但它在状态侧轨所在的 trailing 边缘前 80 点处止步,得不到竖向栏,系统也不告诉它任何关于折痕或摄像头的信息:无论哪种姿态、探针问多少次,每一次保留区域查询都返回空。12 为了确认这个标记是公平的替代,我还用 Xcode 27.0 本身、针对它自带的 iOS 27.0 SDK 编译了一个精简版探针。它得到的窗口一模一样:合上 386 × 678,展开 871 × 669。12
在这一点上,Apple 的文字指南没有讲座那么精确,而且措辞改过。9 月 17 日时,它的概述写的是:“Build your app with Xcode 27.1 or later to use all of the available screen space on iPhone Duo. In earlier versions, your app doesn’t extend under the status bar and camera.”(使用 Xcode 27.1 或更高版本构建,才能用上 iPhone Duo 的全部屏幕空间;在更早的版本中,应用不会延伸到状态栏和摄像头下方)到 9 月 19 日,它改成了与 10 月 2 日相同的说法:“Build your app with the latest version of Xcode to use all of the available screen space on iPhone Duo. When you build with Xcode 26 and earlier, your app doesn’t extend under the status bar and camera.”(使用最新版本的 Xcode 构建;用 Xcode 26 及更早版本构建时,应用不会延伸到状态栏和摄像头下方)2 新句子只点名 Xcode 26 及更早版本不够,而“latest version”也没说 beta 算不算,因此读者可能以为 Xcode 27.0 的构建就能拿到整块屏幕。在这个 beta 的模拟器里,并不能。它得到的是讲座所说的中间一档,而旧措辞与我的测量结果相符。在真机给出不同结论之前,请按表格来规划。
所以,先做这件事:检查您正在测试的构建上的标记。
otool -l Build/Products/Debug-iphonesimulator/YourApp.app/YourApp | grep -A4 LC_BUILD_VERSION
Xcode 的调试构建把代码放在一个小启动器旁边的 YourApp.debug.dylib 里,两者都带有这条命令。您要找的是 sdk 27.1 或更高;标记为 27.2 的探针(也就是 Xcode 27.2 beta 的 SDK 会写入的值)得到了与 27.1 相同的待遇。12 Kiradex 的结果是 minos 27.0 和 sdk 27.1:它可以安装在 iOS 27.0 上,在 Duo 上获得 27.1 的行为。13
我之所以反复强调这一点,是因为我曾公开弄错过。9 月 21 日,我的第一篇关于这个模拟器的报告说工具栏从未变成竖向,保留区域在每种姿态下都为空。beta 本身没有问题。我当时是直接调用 swiftc -sdk 编译那个探针的,链接步骤把同一个 Xcode 里的 macOS SDK 当作了根目录,二进制文件出来时被标记成了 27.0:那天我测到的一切都是中间那一列。那篇文章现在已附上更正。12 如果您的 Duo 布局在模拟器里看起来只采用了一半,栏横在顶部,侧边一条死黑的带子,那么先查标记,再查代码。
工作台上的应用
Kiradex 是一款集换式卡牌收藏应用,我们正在 TestFlight 上测试它:收录每一个系列,记录您已有的卡和想要的卡,追踪它们的价值走势,每张卡还是一个可以翻转查看的 3D 对象。它免费、仅限 iPhone、运行于 iOS 27.0 及更高版本,收藏者的清单保存在他们自己的 iCloud 中,中间没有我们的服务器。13 它还没有完成。扫描功能从未接触过真实的摄像头,而且直到本周,还没有人在展开的 Duo 上看过这款应用:在内屏布局旁边,它的路线图里写着一句“Seen on the open Duo’s inner display only by the column maths; the simulator was folded shut.”(展开的 Duo 内屏只通过列数计算看过;模拟器一直是合上的)13 这让它成为一次公平的测试。其中的 Duo 适配工作完全是照着 Apple 文档写的,从未在屏幕上验证过。
它为 Duo 做的事情很少,值得列出来,正是因为其中 Duo 专用的代码少得可怜:
- 导航交给系统。 一个有五个标签页的
TabView,其中一个是搜索角色,四个标签页里各有一个NavigationStack;扫描器是一个没有自己的栏的相机视图。工具栏项目都是带标题和符号的Label,用.toolbar挂上。任何地方都没有自定义的栏。 - 布局跟随尺寸类别。 compact 宽度下是三列的卡册页面,卡牌的检查器会推入导航栈。regular 宽度(对仅限 iPhone 的应用来说,基本就意味着 Duo 的内屏)下是一面偶数列的卡牌墙;选中一张,屏幕就分成页面和检查器两部分。
- 两处来自 27.1 SDK 的调用。 分栏是
.split样式的ArrangementView,在 iOS 27.1 以下回退为HStack。onHingeChange则在手机从合上变为其他任何状态时,重放应用的开场动画:一台红色设备旋开。 - 没有方向锁定,有启动屏幕。
Info.plist列出了竖屏和两个横屏方向,并声明了UILaunchScreen。13
全部就这些:两个文件里的两处可用性检查。除此之外,手机对这款应用所做的一切,对任何以这种方式构建的应用都会照样去做。
本文中的图片展示的是应用实际运行的样子,卡牌图片来自它读取的公开目录。Kiradex 是一款独立的收藏工具,与 Nintendo、Creatures Inc.、GAME FREAK inc. 或 The Pokémon Company 没有关联,也未获其认可,卡牌图片归各自的权利人所有。
第二步:走遍每种姿态
Apple 的指南把这一步归纳为四项检查:2
- “Confirm your views resize well in each supported orientation and pose.”(确认视图在每种支持的方向和姿态下都能正确调整尺寸)
- “Inspect how the system presents your app’s navigation bars, toolbars, and tab bars vertically on the side of the display.”(检查系统如何把应用的导航栏、工具栏和标签栏竖向呈现在屏幕侧边)
- “Identify any views, sheets, or popovers that position awkwardly when you fold or open iPhone Duo.”(找出折叠或展开 iPhone Duo 时位置别扭的视图、表单或弹出框)
- “Identify elements or controls in your views that appear in the fold, and are difficult to see or interact with.”(找出落在折痕上、难以看清或操作的元素和控件)
它的设计指南说明了需要多少种布局:“A compact width layout for the outer display and a regular width layout for the inner display give you the fundamentals for every pose.”(外屏用 compact 宽度布局、内屏用 regular 宽度布局,就具备了应对每种姿态的基础)7
姿态是 Device Hub 里的按钮:Closed、Book、Open 和 Rotate Right,每种组合都是一块不同的屏幕。SwiftLee 的指南补充了一个更精细的控制,我没有用到:“Holding Option ⌥ reveals a slider that you can use to control the hinge angle of the device with precision”(按住 Option ⌥ 会出现一个滑块,可以精确控制设备的铰链角度)。17 在看应用之前,先了解系统在每种姿态下交给任何应用的是什么,会很有帮助。以下是标记为 27.1 的探针读到的数值:12
| 姿态 | 窗口(点) | 尺寸类别(宽,高) | 栏 | 报告的保留区域 |
|---|---|---|---|---|
| Closed | 466 × 678 | compact,regular | 竖向,trailing 边缘 | 外屏摄像头,37 × 37,活动;状态列,84 × 170,活动 |
| Closed,旋转 | 678 × 466 | compact,compact | 竖向,trailing 边缘 | 外屏摄像头,活动;状态区,84 × 82,活动 |
| Open | 951 × 669 | regular,regular | 竖向,trailing 边缘 | 折痕,宽 40,x 从 455 到 495,非活动;内屏摄像头,58 × 37,非活动;状态列,84 × 120,活动 |
| Book | 951 × 669 | regular,regular | 竖向,trailing 边缘 | 同样三项,折痕为活动 |
| Open,旋转 | 669 × 951 | regular,regular | 横向 | 折痕,高 40,y 从 455 到 495,非活动;内屏摄像头,非活动;状态区,134 × 82,活动 |
| Book,旋转 | 669 × 951 | regular,regular | 横向 | 同样三项,折痕为活动 |
表中有三点决定了之后的一切。
手机折起时,窗口不变。 Book 和 Open 报告的尺寸相同。应用能看到的唯一区别是:折痕的 division 从非活动变为活动,铰链从完全展开的 180 度变为部分展开的 127 度。只读取自身尺寸的应用,永远不会知道手机被折起来了。
折痕一直都在,而且很小。 无论展开还是折起,它都报告在正中间,API 返回的框架在两种状态下都是 40 点宽,两侧各留 20 点边距。Apple 的讲座在谈到折痕区域时说:“When flat, it’s inactive and has a width of zero.”(平放时它是非活动的,宽度为零)(7:32)模拟器并不返回零宽度的框架。Artem Novichkov 的实地笔记对同一读数的描述是“40 pt wide, with 20 pt margins on each side of a zero-width fold line,”(宽 40 pt,零宽度折痕线两侧各有 20 pt 边距)17 我也同样把讲座里的零理解为两段边距之间的那条线;这是我的解读,并非 Apple 原文所说。讲座也点出了实际要点:“By default, only active ones will be returned, but you can query for inactive ones”(默认只返回活动区域,但也可以查询非活动区域),以及“in grid-like layouts, you could prefer even numbers of columns when a division region is present regardless of its active state.”(在网格类布局中,只要存在 division 区域,无论是否活动,都可以优先使用偶数列)(7:16,7:41)5
区域会迟到。 每次启动时,探针的前几次求值完全读不到区域,之后的每次求值都能读到完整的一组。只在第一次布局时问一次的视图,会把一个空数组缓存下来。请在视图的 body 中、每次求值时读取;探针用一个每秒一次的定时器重新求值,而 Artem Novichkov 的笔记把这条规则说得很直接:“Read them in the GeometryReader body so the view updates when they arrive. Don’t cache them.”(在 GeometryReader 的 body 中读取,这样它们到达时视图会更新。不要缓存)1217
看应用之前,还有一个实际提醒。这个 beta 里的 iOS 27.1 模拟器运行时只支持一种设备类型,就是 iPhone Duo;向它请求 iPhone 18 Pro Max 会以“Incompatible device”失败。因此下面的对比中,在普通 iPhone 上运行同一构建的那一组,用的是 iOS 27.0 运行时,这也顺便快速验证了 #available 的回退路径能正常工作。12

同一个 Kiradex 构建分别运行在 6.9 英寸 iPhone、合上的 Duo 和展开的 Duo 上。应用代码里没有任何东西把栏放到侧边。
走查发现了什么
一个 UI 测试带着构建 5 在六种姿态下各走过八类屏幕(合上并旋转时为七类,因为 Settings 按钮被移进了溢出菜单),同时一个脚本在每个停靠点截取两块屏幕:外屏 1398 × 2034 像素,内屏 2853 × 2007 像素,正是 App Store Connect 列出的尺寸。13 对照 Apple 的四项检查,这款应用通过的项目比我预想的多,失败的地方则是任何代码阅读都没有标记出来的。
白得的部分
栏。 合上时,工具栏和全部五个标签都立在时钟下方的侧轨里;展开时也一样;展开并旋转后,它们回到顶部和底部。应用没有为此请求任何东西。第二场讲座解释了手工搭建的栏为何会被落下:“When used to build custom bars, content from sub-components like UIToolbar, UINavigationBar, or UITabBar won’t be considered.”(用于搭建自定义栏时,UIToolbar、UINavigationBar 或 UITabBar 等子组件的内容不会被纳入考虑)(2:52)4 而且由于主屏幕上的每个工具栏项目都是 Label,每一项都有供侧轨使用的符号和供溢出菜单使用的标题,这正是指南的另一个条件:“If your item has a title and doesn’t have an icon, the system doesn’t present it vertically.”(如果项目有标题但没有图标,系统不会把它竖向呈现)2
折痕。 展开时,一个系列的卡牌每行六张,是偶数,所以没有卡牌压在屏幕正中。选中一张,屏幕就在应用内容区的中点 x 433 处分成页面和卡牌检查器。这比折痕偏左 42 点,因为侧轨从右侧占去了 84 点;手机平放时这无关紧要。在 Book 姿态下,arrangement 视图把分隔线移到折痕上,并空出那条 40 点的带子:原本从 x 433 开始的检查器,现在从 495 开始。应用里完全没有针对 Book 姿态的代码;这是 ArrangementView 在做讲座所描述的事,“moving, resizing, or reorganizing what’s already there.”(移动、调整尺寸或重新组织已有的内容)(6:04)513

先是展开平放,再是 Book 姿态。第二张图里的空隙就是折痕,不是应用放进去的。
展开时的表单。 在内屏上,表单居中出现,Done 按钮保持横向;在 Book 姿态下,探针的表单自行移到了折痕的 leading 一侧。12
作为事件的铰链。 应用运行时展开手机,开场动画会在内屏上再播放一次:那台红色设备先是合着,然后旋开,进入应用。实现它的是一个在状态离开 closed 时触发的 onHingeChange 处理器,这正是第四场讲座所要求的分工:“Hinge data is observed live, and is ideal for driving interactions or effects. For layout, use the arrangement and region APIs.”(铰链数据是实时观察的,非常适合驱动交互或效果。布局请使用 arrangement 和区域 API)(2:34)6

在应用运行时展开:开场动画里是应用自己画的设备,由铰链触发重放。
漏掉的部分
只有一个文字按钮的表单,会预留侧轨却让它空着。 合上时,Settings 表单沿 trailing 边缘让出一条给竖向栏的带子,然后什么也不放进去:唯一的工具栏项目是一个只有标题、没有符号的“Done”,而纯文字项目会保持横向。表单被挤进剩下的空间。探针测出了代价:预留侧轨时内容宽度为 374 点,关闭竖向栏后为 450 点。12 Apple 的讲座专门提到了这种情况:“if a sheet is control-heavy with only one item, like the close button here, consider disabling a vertical bar so it doesn’t reduce available space.”(如果表单以控件为主、只有一个项目,比如这里的关闭按钮,可以考虑禁用竖向栏,以免压缩可用空间)(14:37)4 修法有两种,探针都试过。给按钮一个符号,Button("Done", systemImage: "checkmark"),它就会作为一个醒目的对勾移进侧轨;或者给表单内容加上 .toolbarVerticalBehavior(.disabled),侧轨随之消失,代价是表单变矮。

从左到右:应用的表单,然后是探针的三种情况:纯文字 Done、带符号的 Done、禁用竖向栏。
全屏卡牌伸到了摄像头下面。 这款应用的招牌是一张放在黑色展台上、隐藏了栏的卡牌,而这个展台在每条边上都忽略了安全区域。在外屏上,这让卡牌的上角伸到了摄像头下面。系统会向任何询问的视图报告这个摄像头,作为一个 37 点见方的活动 occlusion,其位置与 Apple 自家边框素材中的开孔吻合,误差在 1.5 点以内。1112 Apple 的设计指南允许全宽处理,“as long as nothing conflicts with the Dynamic Island or the status bar.”(只要不与 Dynamic Island 或状态栏冲突)7 修法是让黑色底色保持满版出血,而把卡牌本身收进来:要么让展台在这块屏幕上遵守 trailing 和顶部的安全区域,要么读取 reservedRegions(kind: .occlusion),把卡牌放在返回区域之外。

放进 Apple 外屏边框里的查看器截图。模拟器的截图上没有孔,手机上有。
旋转后,两个窗格上下堆叠。 展开并转成竖屏后,屏幕宽 669 点,仍是 regular 宽度,所以应用照样分栏。但一个填满高大于宽空间的 arrangement 视图会上下分割,Apple 的指南写道:“it places the primary view on top and the secondary view below it when the containing view is taller than it is wide.”(当容器视图高大于宽时,它把主视图放在上方,把次视图放在下方)2 结果是检查器上方一条窄带,只露出一行卡牌和第二行的顶端。应用在那一行代码上的注释说这一对视图“is only useful side by side,”(只有并排时才有用),却没有任何机制来保证这一点。用 .split.axes(.horizontal) 限制样式是文档给出的控制方式,16 但讲座点明了一个陷阱:“If the split arrangement cannot split among an axis, and it’s the primary axis, the arrangement view chooses to only show a single view.”(如果 split arrangement 无法沿某个轴分割,而那正是主轴,arrangement 视图会选择只显示一个视图)(12:44)5 探针证实了这两半:填满旋转后的屏幕时,普通 split 上下堆叠,而仅限横向的 split 只显示主窗格,别的什么也没有。12 对这款应用来说,那会把检查器藏起来,所以老实的修法是一个决定,而不是一个修饰符:当空间高大于宽时,像 compact 布局那样把检查器推入导航栈。

旋转后:栏是横向的,正如 Apple 的指南所说;而分栏朝着对这对视图来说错误的方向去了。
跨越折痕带过来的卡牌,两种布局都不属于。 合上时,选中一张卡牌会把它的检查器推入导航栈。在检查器显示时展开手机,卡牌仍然在,这恰恰是容易出错的部分。但它依旧是一个被推入的屏幕,现在宽 951 点:左边是展台,详情装在一个漂在黑底上的白框里,侧轨里有一个返回按钮。应用为这块屏幕设计的是卡牌墙加旁边的卡牌,而唯一的到达方式是返回再重新选一次卡牌。一个选中状态驱动两种布局,一条导航路径却做不到。修法是在选中卡牌的情况下察觉到从 compact 变成 regular 宽度,把推入的屏幕弹出,让分栏接管。

同一张卡牌,展开前与展开后。状态保留了下来;布局却是被拉伸的 compact 布局。
regular 宽度不等于“Duo”。 这款应用把 regular 宽度当作内屏:卡牌墙、偶数列、分栏。横放的 6.9 英寸 iPhone 同样是 regular 宽度,高度为 compact,而这款应用没有锁定方向。在那里运行,同一个分支会产生一个从左边缘缩进的页面、一个详情列窄到放不下卡牌名称的检查器,以及在屏幕底边被截断的操作按钮。这些问题并非 iOS 27 才有,在我跑过的任何 Duo 姿态中也都没有出现;Duo 适配之所以发现了它,是因为这是第一次有人在每一块报告 regular 宽度的屏幕上走查 regular 宽度布局。区分两者的检查是另一个尺寸类别:内屏在两个方向上都是 regular,而 Apple 的讲座给出的侧放外屏在两个方向上都是 compact。3 需要在两个方向上都有空间的布局,应该两个都问。

regular 宽度布局在一台不是 Duo 的手机上:横放的 6.9 英寸 iPhone,iOS 27.0。
键盘盖住了侧轨。 在外屏上弹出键盘时,侧轨底部的标签会被挡在后面。这是系统的布局,不是缺陷;讲座说栏可能需要溢出,“as other competing UI appears, like the keyboard”(当键盘等其他争夺空间的界面出现时)(11:50)。4 它出现在这份清单上,是因为它弄坏了工具:我的第一次巡览在搜索键盘弹出时点了“Collection”,结果点在了字母 P 上。假设标签栏始终可以点到的 UI 测试,会最先在这里失败。
还有两件小事。这款应用根据宽度尺寸类别来选择偶数列,而讲座的建议是看折痕自身的区域是否存在。另一件与折叠无关:在 6.9 英寸模拟器上连续打开全屏查看器四次,第一次显示了卡牌,后三次是一片空白的黑屏。两者都不至于卡住 TestFlight 构建。两者在应用被放上屏幕、有东西按顺序去按它的按钮之前,都是看不见的。
模拟器展示不了的东西
扫描器。它打开后置广角摄像头,并且已经使用了旋转协调器,这正是相机讲座的要求:“On iPhone Duo, the rotation coordinator will update when your app moves displays.”(在 iPhone Duo 上,当应用移到另一块屏幕时,旋转协调器会更新)(8:19)6 会话能否在从一块屏幕移到另一块屏幕时存活下来,是真机才能回答的问题;模拟器根本没有摄像头。Apple 的发布说明把 StandBy 和大多数应用扩展也列为这个运行时无法运行的内容,而 Xcode 27.2 beta 2 的说明又为自动化截图的人补充了一条:“Screenshots and recordings in iPhone Duo may be black for up to a few minutes after booting the device.”(iPhone Duo 中的截图和录屏在设备启动后最多几分钟内可能是黑的)20
一套源代码,两个 Xcode
日历带来了一个问题。商店接受的构建来自 Xcode 27 和 27.0 SDK;在 iPhone Duo 上表现正确的构建来自 27.1 SDK。在 Apple 向后者开放商店之前,一款既要发布更新、又要继续做 Duo 布局的应用,必须能在两者下都编译通过。
if #available(iOS 27.1, *) 解决不了这个问题。可用性是运行时的问题;编译器仍然得找到这个符号。Kiradex 正是这样包裹它的两处 Duo 调用的,部署目标是 27.0,因此这个构建可以装在尚未升级的手机上。在 27.1 SDK 下,它能编译。在 Xcode 27.0 下,做同样事情的文件会停在 error: cannot find 'ArrangementView' in scope,因为 27.0 SDK 里没有这个类型。14 Kiradex 还没上架,所以它可以一直待在 beta 工具链上;已经有用户的应用不行。
Swift 文档中的条件也区分不了这两个 SDK。#if compiler(>=6.4) 在两者下都为真:Xcode 27.0、Xcode 27.1 beta 和 Xcode 27.2 beta 打印的都是同一个 Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)。14 剩下两种隔离方式,我都试过。
自己设置的编译条件。 这是文档中的做法。只在用 beta 构建的配置里定义一个标志(在 Xcode 中是 SWIFT_ACTIVE_COMPILATION_CONDITIONS = DUO_SDK;在命令行上是 -D DUO_SDK),再用它把仅限 27.1 的代码围起来:
struct Pair<Primary: View, Secondary: View>: View {
@ViewBuilder var primary: Primary
@ViewBuilder var secondary: Secondary
var body: some View {
#if DUO_SDK
if #available(iOS 27.1, *) {
ArrangementView { primary } secondary: { secondary }
.arrangementViewStyle(.split)
} else {
HStack(spacing: 0) { primary; secondary }
}
#else
HStack(spacing: 0) { primary; secondary }
#endif
}
}
不加标志时,这个文件在 Xcode 27.0 和 27.1 beta 下都能通过类型检查。加上标志后,它在 beta 下通过,在 27.0 下则以同样的符号缺失错误失败:错误的搭配根本构建不出来。14
由编译器读取 SDK 自身的版本。 canImport 接受一个带下划线的第二参数,用来与模块版本比较,而 SwiftUI 的模块版本在各个 SDK 中不同:27.0 SDK 中是 8.0.84.1.104,27.1 SDK 中是 8.0.85.27,27.2 beta SDK 中是 8.1.6.1.101。14 因此这种写法不需要任何构建设置:
#if canImport(SwiftUI, _version: 8.0.85)
// ArrangementView, reservedRegions, onHingeChange, toolbarVerticalBehavior
#else
// what the app did before
#endif
我在每个分支里放了一个 #warning,然后用三个 Xcode 分别编译:27.0 走了第二个分支,27.1 beta 和 27.2 beta 走了第一个。14 需要警惕的是那个下划线。Swift 手册中这个条件的语法只有 canImport(import-path),别无其他,因此 _version: 是一个没有文档约定的编译器特性,而 8.0.85 这个数字是我从两个 SDK 中读出来的,并非 Apple 公布的。14 如果在您的环境里加构建设置不方便,可以在商店对 27.1 关闭期间用它;如果您想要一个经得起追问的方案,就用标志。
无论哪种方式,这道隔离都是临时的。等到 App Store Connect 接受带 27.1 SDK 的 Xcode 所构建的应用那一天,把它删掉,保留 #available 检查,因为保护仍停留在 iOS 27.0 的手机的正是后者。
提交:App Store Connect 目前接受什么
TestFlight 接受 27.1 构建。 App Store Connect 9 月 18 日的发布说明写道:“You can now submit apps built with Xcode 27.1 beta using the SDK for iOS 27.1 beta or iPadOS 27.1 beta for internal and external testing.”(现在可以提交使用 iOS 27.1 beta 或 iPadOS 27.1 beta SDK、以 Xcode 27.1 beta 构建的应用,用于内部和外部测试)9 月 16 日和 9 月 28 日的条目就 Xcode 27.2 beta 和 beta 2 说了同样的话。8 Kiradex 就是这样上传的;它的第五个构建,也就是本文测试的这个,于 10 月 2 日上传,App Store Connect 显示它有效。13
App Store 暂时还不接受。 最近一条向商店开放的条目是 9 月 14 日的:“You can now upload apps built with Xcode 27 using the SDK for iOS 27.0, iPadOS 27.0, macOS 27.0, tvOS 27.0, visionOS 27.0, and watchOS 27.0 for the App Store, and for internal and external testing through TestFlight.”(现在可以上传使用 iOS 27.0 等 SDK、以 Xcode 27 构建的应用,用于 App Store,以及通过 TestFlight 进行内部和外部测试)8 此后没有任何条目在同一句话里同时提到 27.1 和商店;而 Apple 的 Releases 信息源(最新条目日期为 9 月 28 日)只列出一个 Xcode 27.1 构建,即 9 月 18 日的 beta。8 Apple 没有说这何时会改变。设备将于 10 月 23 日发售。1
除非 Apple 在 10 月 23 日之前向 27.1 SDK 开放商店,否则用户在发售当天拿到的商店构建,最多也只是一个 27.0 SDK 构建;它在模拟器里能得到什么,上面的对比已经说明,Apple 的讲座描述的也是同样的中间一档:状态侧轨旁边的屏幕、横向栏、没有折痕。只要尺寸调整做得好,这就是一款能用的应用,这也正是现在就用 Xcode 27 发布尺寸调整工作的理由。
启动屏幕现在是强制要求。 这不是 Duo 的规则,但它与同一个 SDK 一起到来,而且在上传时就会失败,而不是在审核时。Apple 的技术说明写道:“Starting in iOS 27 and iPadOS 27, App Store Connect requires your app to include a launch screen configuration in its Info.plist,”(从 iOS 27 和 iPadOS 27 开始,App Store Connect 要求应用在其 Info.plist 中包含启动屏幕配置)如果 UILaunchStoryboardName、UILaunchStoryboards、UILaunchScreen 或 UILaunchScreens 一个都没有,上传会被拒绝,并显示“ITMS-90870: Missing launch screen.”10 Kiradex 声明了 UILaunchScreen,并配上一个颜色,即封面的红色,因此启动后会直接衔接开场动画。13
截图位置在纸面上已经存在。 App Store Connect 的截图规格自 9 月 9 日起就列出了 iPhone Duo,分为两组:外屏为 1398 × 2034 或 2034 × 1398 像素,内屏为 2007 × 2853 或 2853 × 2007 像素,并附注“Support for uploading assets for this device in App Store Connect will be available later this year.”(在 App Store Connect 中上传此设备素材的支持将于今年晚些时候提供)9 常规规则照旧适用:“You can upload one to 10 screenshots in .jpeg, .jpg, and .png formats,”(可以上传 1 到 10 张 .jpeg、.jpg 和 .png 格式的截图)以及“Images can’t include alpha channels or transparencies.”(图片不能包含 Alpha 通道或透明区域)9
上传页面上的一句话决定了您该如何围绕这一点做计划:“Once your app is submitted for review and approved, you must create a new version to update the screenshots.”(应用提交审核并获批后,必须创建新版本才能更新截图)9 Duo 截图没法事后单独补进去;等位置开放时,它们得随一个版本一起上。
综合起来,我会这样安排,Kiradex 也正在这样做:
- 现在就把 27.1 构建放进 TestFlight,走查中发现的问题随发现随修。
- 对于已经上架的应用:用 Xcode 27 构建一个更新,包含所有不需要 27.1 SDK 的修复。用尺寸类别代替 idiom 和方向判断,按边处理安全区域,栏交给系统容器,每个工具栏项目都配标题和符号。
- 现在就从模拟器按 Duo 尺寸截好图,保存起来。
- 准备好一个版本,等待两件事同时成立的那一天:商店接受带 27.1 SDK 的 Xcode,并且 Duo 的截图位置可以上传。到目前为止,这类变化 Apple 每次都发布在 App Store Connect 的发布说明里,包括 9 月 14 日向 Xcode 27 正式版开放商店的条目,以及 9 月 9 日添加 Duo 截图规格的条目,所以那就是要盯的页面,旁边再开着 developer news 页面。8
还有一项要求在更远处。从 2027 年 4 月起,要上传任何东西,“iOS and iPadOS apps must be built with the iOS 27 & iPadOS 27 SDK or later”(iOS 和 iPadOS 应用必须使用 iOS 27 和 iPadOS 27 SDK 或更高版本构建)。18 仍用 iOS 26 SDK 构建的应用,在 Duo 上得到的是上面三种画面中最小的那一种,而且从 2027 年 4 月起,在迁移到 iOS 27 SDK 之前将无法上传更新。
为两块屏幕准备截图
走查已经产出了原始素材:每个停靠点都按 1398 × 2034 和 2853 × 2007 像素截了图,旋转后则是 2034 × 1398 和 2007 × 2853,App Store Connect 的 iPhone Duo 那一行里的四种尺寸全都齐了。913 剩下的是决定在它们周围放什么,而折叠屏手机恰恰在这里诱人犯错。
人人都想要的画面,是手机斜着半开,应用从折痕上倾泻而出。Apple 的营销准则逐条排除了这种做法。关于它自己的设备图片:“Use Apple product images ‘as is’ and without modification. Modifications include adding reflections, shadows, highlights, or graphic elements that appear to enter or come out of the product screen; cropping, tilting, or obstructing any part of the images; animating, flipping, or spinning the images”(按“原样”使用 Apple 产品图片,不加修改。修改包括添加反射、阴影、高光,或看似进出产品屏幕的图形元素;裁剪、倾斜或遮挡图片的任何部分;对图片做动画、翻转或旋转)。关于您自己制作的图像:“Straight-on product shots are preferred. Don’t use extreme angles or alter an Apple product in any way.”(首选正面产品照片。不要使用极端角度,也不要以任何方式改动 Apple 产品)。而在 Unauthorized Uses 下,排在第一位的是:“Rendering in 3D or creating any simulation of an Apple product”(对 Apple 产品进行 3D 渲染或制作任何模拟)。11 我的截图文章详细讨论了获奖作品如何对待这些规则,以及遵守规则的一组截图是什么样子;多了一个铰链,这些规则也不会变。
Apple 给您的替代品,是这款手机的边框素材包,有两种外观、五种视图:竖放和横放的合上手机,横放和竖放的展开内屏,以及从背面看的展开手机,外屏位于摄像头旁边。这些文件中的开口恰好就是截图尺寸,所以模拟器截图无需缩放即可放进去。11 没有半折的视图,也没有倾斜的视图。
所以下面的画面由三部分构成:应用自身配色的底色、一行简短的文字,以及放在 Apple 边框里的截图,完整、竖直、上面不覆盖任何东西。变化来自底色、设备的大小,以及每组中一张完全不含设备的画面:黑底上的应用全屏卡牌,那是应用自己的 3D,不是谁家的硬件。

由脚本根据巡览截图合成的三组画面,尺寸分别为 1320 × 2868、1398 × 2034 和 2853 × 2007 像素。它们展示的是方法,不是正式的商店页面:这些截图里有目录中的卡牌,商店那组截图该展示什么,正是下面第三条提到的悬而未决的问题。
制作过程中得出的三条实用建议。
用脚本合成。 上面这几组画面出自一个 Python 文件:输入一张截图、一句标题和一种底色,输出精确尺寸、不带 Alpha 通道的 PNG。应用一变(这款应用在我截图的当天就变了两次),重新跑一遍巡览,画面就会重新生成。
截下手机额外提供的东西。 Apple 的上传页面说,如果界面在各种尺寸上都一样,一组 6.9 英寸截图就够了:“provide only the highest resolution screenshots required. They automatically scale down to smaller device sizes.”(只需提供所需的最高分辨率截图,它们会自动缩小到更小的设备尺寸)9 在这款手机上,界面并不一样:卡牌墙和分栏检查器只存在于内屏上。内屏那组截图开头的两张就是它们。
留意屏幕上显示的内容。 同一份准则写道:“You are responsible for securing the rights to all materials used in screen content within your app.”(您有责任取得应用屏幕内容中所用全部素材的权利)11 收藏类应用天然会展示他人的美术作品,而商店页面属于营销,这与应用在使用中显示的内容是两回事。我们还没有为 Kiradex 做出这个决定,上面的画面不会原样提交。出于这个原因,巡览还有第二种模式,使用六张虚构的卡牌运行;它们目前还没有美术图,所以要提交一组截图,要么设计替代图,要么解决版权问题。
Kiradex 目前还没有商店页面。等到有了,它会从一组 6.9 英寸截图开始,Duo 的截图则随一个独立的版本,等待 App Store Connect 开始接受它们的那一天。
把这项工作交给编程智能体
这项工作的大部分都是智能体擅长的那类事,前提是它能看见应用。工作分两半,其中一半 Apple 已经提供。
静态的那一半属于 Apple。 第一场 Tech Talk 就以此收尾:“During the talk, Modernize Your UIKit App, we introduced a new app modernization skill. With Xcode 27.1, this skill has a new name: App Resizability. It now supports SwiftUI and iPhone Duo.”(在 Modernize Your UIKit App 讲座中,我们介绍了一个新的应用现代化技能。在 Xcode 27.1 中,这个技能有了新名字:App Resizability。它现在支持 SwiftUI 和 iPhone Duo)(9:19)3 这个技能是 beta 中的一组纯文本文件:在 Xcode 27.1 beta 里,是 Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/app-resizability.idechatprompttemplate 及其旁边的五个参考文件。15 它的说明规定了撒网的范围:“Treat a request about the foldable iPhone Duo as a request for every task in the Task Registry, because a screen that changes size exposes all of them at once.”(把关于可折叠 iPhone Duo 的请求视为对 Task Registry 中每一项任务的请求,因为尺寸会变化的屏幕会一次性暴露所有这些问题)15
即使您从不运行它,也值得一读,因为它是 Apple 写成文字的审查清单。它检查三项项目设置,但一项都不修改(启动屏幕键、iPad 方向声明、UIRequiresFullScreen),然后搜寻五种模式:UIScreen.main、由界面方向决定的布局、由 userInterfaceIdiom 决定的布局、本应使用场景生命周期却使用了应用程序生命周期的地方,以及假定两侧相同的安全区域代码。安全区域那个文件里有一句话,解释了这款手机上一半的问题:“Asymmetric horizontal insets are the normal case. Where a vertical bar is present, one horizontal edge usually carries the whole inset and the other carries zero.”(左右不对称的内边距才是常态。存在竖向栏时,通常一侧承担全部内边距,另一侧为零)15
Kiradex 是今年写的 SwiftUI 应用,这五项搜索在它身上什么也找不到。13 上面的每一项发现都来自运行它。这就是静态那一半的局限:它读的是源代码,而折痕、侧轨和键盘只有在屏幕上才看得见。
另一半是一次智能体可以运行、可以查看的走查。 让上面这次审查成为可能的是三个小部件,没有一个是这款应用特有的:
- 一个访问每类屏幕并在每处停下的 UI 测试。 不做断言,只做巡览。我们的巡览依次打开 Dex、一个系列、一张卡牌、一个表单、收藏列表、收藏中的一张卡牌、全屏查看器,以及弹出键盘的搜索:共八个停靠点。13
- 在 Mac 上而不是在测试里截图。 UI 测试自己的截图只覆盖一块屏幕。
simctl两块都能拿到:
xcrun simctl io "$UDID" screenshot --type=png --display=primary outer.png
xcrun simctl io "$UDID" screenshot --type=png --display=primary-1 inner.png
测试写下一个以停靠点命名的空文件;Mac 上的一个 shell 循环看到它,截取两块屏幕,再写一个文件作为回应;测试等到这个文件后继续往下走。外屏截图在手机合上时为 1398 × 2034 像素,内屏截图在展开时为 2853 × 2007 像素,因此审查用的图片和商店截图出自同一次运行。913
3. 一种不用手碰鼠标就能切换姿态的方法。 simctl 没有姿态命令,这个 beta 中的 UI 测试框架里,就我所能找到的范围,也没有铰链调用,所以脚本通过辅助功能 API 按下 Device Hub 的 Closed、Book、Open 和 Rotate Right 按钮,即使 Device Hub 在后台也能工作。13
有了这些,检查应用在每种姿态下的表现,就不再是征求智能体的意见,而变成每种姿态一个图片文件夹,它和您都能阅读。下面是我会交出去的任务说明,写成了可以直接粘贴的形式。
Goal: make this app correct on iPhone Duo in every pose, without device checks.
0. Toolchain. Build with Xcode 27.1 beta. Confirm the binary is stamped with the 27.1 SDK:
otool -l <App>.app/<App> | grep -A4 LC_BUILD_VERSION (debug builds: <App>.debug.dylib)
If "sdk" is lower than 27.1, stop: nothing below will reproduce.
1. Static pass. Report every use, with file and line, of:
UIScreen.main / UIScreen.mainScreen; userInterfaceIdiom; interface orientation used for layout;
a UIApplicationDelegate doing scene work; one safe-area inset applied to both sides;
a bare ignoresSafeArea() on anything a person reads or taps; fixed widths tied to a phone size;
toolbars or tab bars built by hand instead of owned by NavigationStack, NavigationSplitView or TabView;
toolbar items with a title and no symbol, or a symbol and no title.
Confirm Info.plist has a launch screen key and no orientation lock the design does not need.
2. Walk. Run the screenshot tour in each pose and capture BOTH displays at every stop:
closed; closed and turned; open; open and turned; partly folded; partly folded and turned.
If no tour exists, write a UI test that stops at each kind of screen and signals a host script, and have
the script capture with: xcrun simctl io <udid> screenshot --display=primary (outer)
xcrun simctl io <udid> screenshot --display=primary-1 (inner)
Poses are buttons in Device Hub (Closed, Book, Open, Rotate Right); simctl has no pose command.
3. Read every capture and answer, per pose:
- Are the bars where the system puts them (down the side closed and in open landscape, across the top
and bottom in open portrait)? Is any toolbar item missing from the bar and from the overflow menu?
- Does any sheet reserve the side rail and leave it empty?
- With the keyboard up, what is covered? Can every tab still be reached once it is dismissed?
- Does anything sit under the outer camera corner or the status column?
- Open: does a grid have an even number of columns? Does any control or line of text cross the middle?
- Partly folded: does content move off the fold? Do sheets and alerts land on one side?
- Turned: did a two-pane layout stack when it should have stayed side by side, or the reverse?
- Is the same selection, scroll position and navigation path still there after the pose changed?
4. Fix with, in this order: a system container that already adapts; size classes; ArrangementView for a
custom two-pane layout; reservedRegions for hand-placed content. Never branch on the device model,
the idiom, the orientation, or the raw hinge angle to decide layout.
5. Guard everything from the 27.1 SDK with `if #available(iOS 27.1, *)` and a fallback that keeps both
panes reachable. If the same source must also build with Xcode 27.0, fence it at compile time.
6. Report what could not be checked in the simulator: cameras, StandBy, extensions, haptics, real reach.
Apple 的资料,按我会使用的顺序
Apple 为这款设备发布的所有资料,都挂在同一个页面 Get ready for iPhone Duo 下面。19 按工作顺序排列如下:
| 资料 | 为什么要打开它 |
|---|---|
| Prepare your app for iPhone Duo(Tech Talk) | 三档 SDK、尺寸类别、安全区域。从这里开始 |
| Preparing your app for iPhone Duo(指南) | 同样的内容以文字呈现,每个 API 都有名字。“Address common layout and resizing considerations”下的四项检查就是审查清单2 |
| Raise the bar with iPhone Duo | 竖向栏:什么放进去,什么留在外面,溢出 |
| Strike a pose with adaptive layouts on iPhone Duo | 折痕:避让、保留区域、arrangement 视图 |
| Designing for iPhone Duo(HIG)和 Design for iPhone Duo | 设计师会要求您遵守的规则7 |
| Leverage multiple displays and scenes on iPhone Duo | 铰链、Split View、外屏上的第二个场景 |
| Build a great camera experience for iPhone Duo | 仅在您的应用需要拍摄时:两个前置摄像头,以及各自朝向哪边6 |
| Group Lab 录像(第 1 天和第 2 天),以及 SwiftUI、UIKit、Photos and Camera 的论坛问答 | Apple 工程师回答开发者的问题19 |
| Apple Design Resources | Figma 和 Sketch 套件,以及用于营销的产品边框11 |
| Apple 之外:SwiftLee 的模拟器指南、iPhone Duo by Examples、BleepingSwift 的清单、Adapty 的指南 | 一份测试清单和铰链滑块;每个 API 一个可运行的示例,外加实地笔记;一份简短清单;我找到的唯一一份把付费墙也过了一遍的指南17 |
| 工作坊 | 线下参加。SwiftLee 形容它们“with the opportunity to test your app on a physical device,”(提供在实体设备上测试应用的机会),在 10 月 23 日之前,这是检查摄像头的唯一途径1719 |
Group Lab 的问答整理和论坛帖子我没有读:Apple 的论坛页面会对自动化抓取返回一个真人验证页面,我没有去绕过它。
要点总结
- 如果您现在已有上架应用: 现在就把尺寸调整工作用 Xcode 27 构建并发布出去。如果 10 月 23 日那天商店仍未向 27.1 SDK 开放,您的用户在 Duo 上拿到的就是它,而这些工作都不需要 beta。
- 如果您要采用 Duo API: 用 Xcode 27.1 beta 构建,用
otool确认是sdk 27.1或更高,把构建放在 TestFlight,并且如果同一套源代码仍需为商店构建,就把仅限 27.1 的调用隔离起来。 - 如果您负责测试: 不要读代码,去跑姿态。合上、展开、半折、每种都旋转一次,再加一个表单、键盘、一个全屏视图,以及一个跨越折痕带过去的屏幕。每次都截取两块屏幕。
- 如果您负责商店页面: 现在就按 Duo 尺寸截图,用脚本合成,正面使用 Apple 的边框,并准备好一个版本,等待截图位置开放的那一天。
- 如果您要把这项工作交给智能体: 交给它的是走查,而不只是文件。Apple 的 App Resizability 技能涵盖的是靠阅读能发现的问题;上面的每一项发现都是靠看才发现的。
常见问题
我现有的应用不做修改能在 iPhone Duo 上运行吗?
能。Apple 的讲座对任何 SDK 都作出了这一承诺,模拟器也证实了这一点:标记为 iOS 26 SDK 的构建以 375 × 667 点的窗口运行,标记为 27.0 的构建则以横向栏填满状态侧轨旁边的屏幕。两者都得不到竖向栏或保留区域。312
完整的 iPhone Duo 布局需要哪个 Xcode?
Xcode 27.1 beta(27A9269),它包含 iOS 27.1 SDK 和唯一的 iPhone Duo 模拟器。它的 27.1 模拟器运行时只支持 iPhone Duo 设备类型,因此其他 iPhone 要在 27.0 运行时上测试。Xcode 27.2 beta 的 SDK 有相同的 API,标记为 27.2 的探针在模拟器中的表现也与 27.1 一致,但那个 Xcode 没有 Duo 可供运行。81214
现在能把 iPhone Duo 构建提交到 App Store 吗?
截至 2026 年 10 月 2 日,用 27.1 SDK 构建的不行。App Store Connect 接受 Xcode 27.1 beta 和 27.2 beta 的构建用于内部和外部 TestFlight 测试,接受 Xcode 27 的构建用于商店。Apple 尚未公布这一变化的日期。8
App Store Connect 要求 iPhone Duo 使用哪些截图尺寸?
外屏为 1398 × 2034 或 2034 × 1398 像素,内屏为 2007 × 2853 或 2853 × 2007 像素,1 到 10 张,不带 Alpha 通道。上传功能标注为“later this year”(今年晚些时候)提供。9
如何截取 iPhone Duo 模拟器的两块屏幕?
外屏用 xcrun simctl io <udid> screenshot --display=primary,内屏用 --display=primary-1。UI 测试自己的截图只覆盖一块屏幕。13
为什么我的应用在 iPhone Duo 模拟器里的栏仍然是横向的?
有三个原因,按检查顺序排列。二进制文件没有标记为 27.1 SDK。栏是手工搭建的,而不是由 TabView、NavigationStack 或 NavigationSplitView 管理。或者内屏处于竖屏方向,在那里 Apple 按设计保持栏为横向。2712
每种姿态都需要一种布局吗?
不需要。Apple 的指南是两种布局,compact 宽度和 regular 宽度,再加上在内容会横跨折痕的地方对折痕作出响应。Kiradex 有这两种布局和一个 arrangement 视图;Book 姿态不需要任何代码。713
本站相关文章:模拟器文章包含安装步骤、设备配置文件和更正后的各姿态测量值;为 iPhone Duo 设计把 Apple 的设计指南解读为规则;Duo 开发者文章收录了硬件数据和按日期整理的经过;截图文章论证了一组截图应该说什么;可调尺寸 iPhone 清单是本文建议您先发布的 Xcode 27 工作;Xcode 27 汇总页则持续追踪工具链。
来源
-
Apple Newsroom,Apple unveils iPhone Duo,2026 年 9 月 9 日,2026 年 10 月 2 日获取:“Pre-orders begin Friday, October 16, with availability beginning Friday, October 23.”(10 月 16 日星期五开始预购,10 月 23 日星期五开始发售)以及“iPhone Duo will be available with iOS 27.1.”(iPhone Duo 将随 iOS 27.1 提供) ↩↩↩
-
Apple,Preparing your app for iPhone Duo,Technology Overviews,2026 年 10 月 2 日通过文档 JSON 端点获取;引用了 Overview 以及“Address common layout and resizing considerations”“Organize items in your bars”“Arrange views in different poses”各节。本站在 2026 年 9 月 17 日引用时,关于 Xcode 版本的那句 Overview 措辞不同(“Build your app with Xcode 27.1 or later to use all of the available screen space on iPhone Duo. In earlier versions, your app doesn’t extend under the status bar and camera.”);本站的 Duo 开发者文章于 9 月 19 日记录了现在的措辞。该页面没有修订说明。 ↩↩↩↩↩↩
-
Apple Developer Tech Talk,Prepare your app for iPhone Duo,David Jackson,UI Frameworks。引文出自讲座英文字幕轨中标注的时间点。 ↩↩↩↩
-
Apple Developer Tech Talk,Raise the bar with iPhone Duo,Anna(UI Frameworks 工程师)与 Maria(Human Interface Designer)。引文出自讲座英文字幕轨中标注的时间点。 ↩↩↩
-
Apple Developer Tech Talk,Strike a pose with adaptive layouts on iPhone Duo,Maria(Human Interface Designer)与 Harry(UI Frameworks 工程师)。引文出自讲座英文字幕轨中标注的时间点。 ↩↩↩
-
Apple Developer Tech Talks,Leverage multiple displays and scenes on iPhone Duo 和 Build a great camera experience for iPhone Duo,引文出自英文字幕轨中标注的时间点;Apple,Choosing a camera by the direction it faces,AVKit,2026 年 10 月 2 日获取。 ↩↩↩
-
Apple,Designing for iPhone Duo,Human Interface Guidelines,2026 年 10 月 2 日通过文档 JSON 端点获取;引用了“Device poses”和“Vertical controls”两节。 ↩↩↩↩↩
-
Apple,App Store Connect release notes,2026 年 10 月 2 日获取:引用了 2026 年 9 月 14 日和 9 月 18 日的条目;9 月 9 日的条目添加了 iPhone Duo 的截图规格,并说上传“will be available later this year”(将于今年晚些时候提供),规格页面重复了这句话;9 月 16 日和 9 月 28 日的条目以同样的形式向 Xcode 27.2 beta 和 beta 2 开放 TestFlight,涉及六个平台;9 月 18 日之后的条目中没有任何一条提到 Xcode 27.1 或 iOS 27.1 SDK。Apple,Releases,RSS 信息源,2026 年 10 月 2 日获取,最后构建日期 Mon, 28 Sep 2026 14:00:00 PDT:提到 Xcode 27.1 的条目只有一条,即“Xcode 27.1 beta (27A9269)”,日期为 Fri, 18 Sep 2026;最新的 Xcode 条目是“Xcode 27.2 beta 2 (27B5028f)”,日期为 Mon, 28 Sep 2026。 ↩↩↩↩↩↩↩↩↩↩↩
-
Apple,Screenshot specifications 和 Upload app previews and screenshots,App Store Connect Help,2026 年 10 月 2 日获取,有引用。 ↩↩↩↩↩↩↩↩↩↩
-
Apple,TN3208: Preparing your app’s launch screen to meet App Store requirements,2026 年 10 月 2 日获取,有引用;修订历史:“2026-09-14 Updated the ITMS-90870 error message to reflect the iOS 27 launch screen requirement.”(更新 ITMS-90870 错误信息以反映 iOS 27 的启动屏幕要求)以及“2026-06-08 First published.”(首次发布) ↩↩↩
-
Apple,Marketing Resources and Identity Guidelines,“Apple Product Images”“Unauthorized Uses”“Screen Content”和“Custom Photography and Video”各节,2026 年 10 月 2 日获取,有引用。Apple,Apple Design Resources,Product Bezels,iPhone Duo(Photoshop 和 PNG),2026 年 10 月 2 日获取。边框尺寸为作者根据 Apple 的
Bezel-iPhone-Duo.dmg中的 PNG 文件测量所得:“Outer Closed Portrait”为 1574 × 2194 像素,透明开口为 1398 × 2034;“Inner Open Landscape”为 3093 × 2247,开口为 2853 × 2007;素材包中还有“Inner Open Portrait”“Outer Closed Landscape”和“Outer Open”,每种都有 Star White 和 Night Sky 两款。“Outer Closed Portrait”中的摄像头开孔,按每点三像素在开口内测量,横向跨越 400.3 到 436.3 点,纵向跨越 29.7 到 65.7 点;探针报告的摄像头 occlusion 跨越 399 到 436 和 30 到 67。 ↩↩↩↩↩↩ -
作者于 2026 年 10 月 2 日的运行:macOS 27.0(26A428)、Xcode 27.1 beta(27A9269)、iOS 27.1 模拟器运行时(24A94401),以及一台由 iPhone Duo 设备类型创建的模拟器。探针 DuoProbe2 是一个 Swift 文件:一个
TabView,其第一个标签页包含一个带五个工具栏项目的NavigationStack;一个读数视图,显示GeometryProxy尺寸、尺寸类别、toolbarVerticalEdge、onHingeChange,以及带.includeInactive的两类reservedRegions,在 body 每次求值时读取,并每秒重新求值一次;一个 split 样式的ArrangementView;以及一个 UIKit 视图,记录其窗口、窗口场景的屏幕、UIScreen.main和verticalBarEdge特征。它用swiftc针对 27.1 模拟器 SDK 从该文件编译了三次,部署目标为 27.1,并告诉链接器记录哪个 SDK 版本(-Xlinker -platform_version -Xlinker ios-simulator -Xlinker 27.1 -Xlinker <26.0, 27.0 or 27.1>);对每个二进制文件运行otool -l都打印出相应的sdk值。姿态通过辅助功能 API 按下 Device Hub 的 Closed、Book、Open 和 Rotate Right 按钮来设置。控制台输出,27.1 标记,合上:size=382x562 ... h=compact v=regular verticalEdge=trailing safe=EdgeInsets(top: 82.0, leading: 0.0, bottom: 34.0, trailing: 84.0) divisions=0 occlusions=2、occlusion[0] active=true frame=(399,-52 37x37)、occlusion[1] active=true frame=(382,-82 84x170)、window=(466.0, 678.0)、verticalBarEdge=2;展开:size=867x553 ... h=regular v=regular verticalEdge=trailing ... divisions=1 occlusions=2、division[0] active=false frame=(455,-82 40x669) margins=EdgeInsets(top: 0.0, leading: 20.0, bottom: 0.0, trailing: 20.0)、occlusion[0] active=false frame=(677,-61 58x37)、occlusion[1] active=true frame=(867,-82 84x120)、window=(951.0, 669.0)、hinge=fullyOpen 180 deg;Book:相同,但为division[0] active=true和hinge=partiallyOpen 127 deg;展开并旋转:size=669x734 ... verticalEdge=nil ... divisions=1 occlusions=2、division[0] active=false frame=(0,321 669x40)、window=(669.0, 951.0)、mainScreen=(466.0, 678.0)、verticalBarEdge=0;合上并旋转:size=594x350 ... h=compact v=compact verticalEdge=trailing、window=(678.0, 466.0)。框架坐标以读数视图为准,其原点位于窗口顶部下方 82 或 134 点处。27.0 标记:合上window=(386.0, 678.0),展开(871.0, 669.0),展开并旋转(669.0, 871.0),合上并旋转(678.0, 386.0),每种姿态的每一行都是verticalEdge=nil和divisions=0 occlusions=0。26.0 标记:合上、展开、Book 以及展开并旋转时均为window=(375.0, 667.0),合上并旋转时为(667.0, 375.0),全程h=compact。每次以 27.1 启动时,前六到九次求值在区域出现之前记录的是divisions=0 occlusions=0,其中三次发生在视图获得尺寸之前。展开和 Book 姿态下的窗格位置根据截图测量:平放时主窗格从 x 8 到 433.7,次窗格从 433.7 到 859;Book 姿态下主窗格到 455.7,空带到 495.7,次窗格到 859。表单,合上:纯文字 Done 时内容为374x562,Done 带符号时相同,使用.toolbarVerticalBehavior(.disabled)时为450x428且verticalEdge=nil;展开:居中653x501,h=compact;Book:位于 leading 边缘的459x501。当 arrangement 视图填满内容区时(一个启动选项),普通的.split在旋转后的屏幕上把主窗格放在次窗格上方,而.split.axes(.horizontal)只显示主窗格;展开平放时同一视图左右分割,在 Book 姿态下空出折痕的带子。按 9 月 21 日文章所述方式(未设置SDKROOT的swiftc -sdk)编译的探针打印出clang: warning: using sysroot for 'macOS 27.0' but targeting 'arm64-apple-ios27.1.0-simulator',并被标记为sdk 27.0。另有两个构建在合上和展开状态下运行。PlainProbe 具有同样的标签视图、导航栈和工具栏,但不含任何来自 27.1 SDK 的内容,由 Xcode 27.0(27A266a)针对其自带的 iOS 27.0 SDK 编译(otool:minos 27.0、sdk 27.0):合上window=(386.0, 678.0),展开(871.0, 669.0)。标记为sdk 27.2的 DuoProbe2:两种姿态下的输出与 27.1 标记相同。simctl list runtimes -j显示 27.1 运行时的supportedDeviceTypes只有一项,即 iPhone Duo;在该运行时上用 iPhone 18 Pro Max 类型执行simctl create会以“Incompatible device”失败。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 是作者的应用(941 Apps),bundle 版本 1.0,构建 5,于 2026 年 10 月 2 日上传到 TestFlight,并于当天被 App Store Connect 列为有效。项目相关事实来自该构建时的源代码:部署目标 iOS 27.0,使用 27.1 SDK 构建(对应用二进制文件运行
otool -l:minos 27.0、sdk 27.1),带颜色的UILaunchScreen,竖屏和两个横屏方向,一个有五个标签页的TabView,以及两处#available(iOS 27.1, *),一处包裹ArrangementView,一处包裹onHingeChange。路线图那句话引自项目自己的笔记。走查是一个 UI 测试,在八类屏幕处停下,并请主机脚本用simctl io <udid> screenshot --display=primary和--display=primary-1截取模拟器;它在 iPhone Duo 模拟器(iOS 27.1 运行时)上以六种姿态运行,其中五种姿态走完全部八个停靠点,合上并旋转时为七个,另外还在 iPhone 18 Pro Max 模拟器(iOS 27.0 运行时,竖屏和横屏)上运行。截图尺寸:1398 × 2034 和 2034 × 1398(外屏),2853 × 2007 和 2007 × 2853(内屏),1320 × 2868(6.9 英寸)。检查器左边缘根据内屏截图测量,平放时为 x 433.7,Book 姿态下为 495.7。展开测试在合上状态下启动应用,持续截取内屏,然后按下 Open;连续四帧展示了开场动画。带卡测试在合上状态下打开一张卡牌,按下 Open,二十秒后截图。查看器测试在 6.9 英寸模拟器上于一次会话中打开全屏查看器四次并测量每张截图:第一张非黑像素占 52%,其余三张占 0.3%。在 Xcode 27.1 beta 模拟器平台的XCTest和XCUIAutomation框架中以文本搜索“hinge”“posture”“DevicePose”和“foldState”,一无所获,simctl也没有列出任何姿态命令。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者于 2026 年 10 月 2 日的测试。一个在
if #available(iOS 27.1, *)之后使用ArrangementView的文件,用swiftc -typecheck针对每个已安装 Xcode 的模拟器 SDK 做了类型检查:Xcode 27.0(27A266a)以error: cannot find 'ArrangementView' in scope失败;Xcode 27.1 beta(27A9269)和 Xcode 27.2 beta(27B5019j)通过。同一文件用#if DUO_SDK隔离后,在 27.0 下不加标志时通过,在 27.1 beta 下加不加标志都通过,在 27.0 下加-D DUO_SDK时失败。用#if canImport(SwiftUI, _version: 8.0.85)隔离后,在三者下都通过,每个分支中的#warning显示 27.0 编译的是回退分支,两个 beta 编译的是 27.1 分支。xcrun swift --version在三者下都打印Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)。SwiftUI 模块版本取自每个 SDK 的SwiftUI.swiftinterface中的-user-module-version值。这里安装的 27.2 beta 是 beta 1(27B5019j);它的 iOS 27.2 模拟器运行时(24B5084k)没有列出 iPhone Duo 设备类型,Apple 的 27.2 说明让开发者为此使用 27.1 beta。该条件的语法出自 The Swift Programming Language 的 Statements,“Conditional Compilation Block”,2026 年 10 月 2 日获取:“platform-condition →canImport(import-path)”。 ↩↩↩↩↩↩↩↩↩ -
Xcode 27.1 beta(27A9269),
Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/:app-resizability.idechatprompttemplate以及app-resizability-ref-uiscreen-task.md.packaged、-orientation-task、-scene-lifecycle-task、-safe-area-task和-idiom-task,2026 年 10 月 2 日阅读;引文出自模板的“When to Use”一节和安全区域参考文件的第 6 条规则;三项项目检查是其“Prerequisites”表,五种模式是其“Task Registry”。Xcode 27.0(27A266a)的同一文件夹中是uikit-app-modernization.idechatprompttemplate和四个参考文件。 ↩↩↩ -
Apple,2026 年 10 月 2 日通过 JSON 端点获取的文档:ArrangementView、reservedRegions(kind:options:layoutDirectionBehavior:)、onHingeChange(isEnabled:_:)、toolbarVerticalBehavior(_:) 和 toolbarVerticalEdge。 ↩
-
Antoine van der Lee,iPhone Duo Simulator: Testing and optimizing your SwiftUI app,SwiftLee,2026 年 9 月 22 日,有引用。Artem Novichkov,iPhone Duo by Examples,GitHub README,“Good to Know”,2026 年 10 月 2 日获取:“Its frame is the same in both states: 40 pt wide, with 20 pt margins on each side of a zero-width fold line.”(其框架在两种状态下相同)、“Reserved regions arrive after the first layout pass.”(保留区域在第一次布局之后才到达)以及“When folded, the outer display has no reserved regions at all, not even inactive ones.”(合上时外屏完全没有保留区域,连非活动的也没有)。前两条与这里的探针一致;第三条不一致,因为探针在合上的外屏上读到了两个活动的 occlusion。Mick MacCallum,How to Get Your App Ready for iPhone Duo,BleepingSwift,2026 年 9 月 18 日。Yurii Kleimenov,How to adapt your iOS app to iPhone Duo,Adapty,2026 年 9 月 11 日发布,标注 9 月 15 日更新。 ↩↩↩↩↩
-
Apple,“App Store submissions now open for the latest OS releases”,developer news,2026 年 9 月 9 日:从 2027 年 4 月起,上传到 App Store Connect 的应用“need to meet the following minimum requirements”(需要满足以下最低要求),其中第一条是“iOS and iPadOS apps must be built with the iOS 27 & iPadOS 27 SDK or later”。 ↩
-
Apple,Get ready for iPhone Duo,2026 年 10 月 2 日获取:六场 Tech Talk、两段 Group Lab 录像、一个指向 Group Lab 问答的链接、Photos and Camera、SwiftUI 和 UIKit 的论坛问答、Xcode 27.1 beta、设计指南与资源、准备指南,以及线下工作坊。Group Lab 问答页面和论坛帖子未读;对它们的请求返回了真人验证页面。 ↩↩↩
-
Apple,Xcode 27.1 Beta Release Notes,2026 年 10 月 2 日通过文档 JSON 端点获取,Simulator,Known Issues:“StandBy is unavailable in the iPhone Duo Simulator runtime. (187708663)”以及“Running and debugging most app extensions is unavailable in the iPhone Duo Simulator runtime. (187708767)”。Apple,Xcode 27.2 Beta 2 Release Notes,2026 年 10 月 2 日以同样方式获取:Overview,“Download Xcode 27.1 beta to get the iOS SDK and simulator support for iPhone Duo.”(下载 Xcode 27.1 beta 以获得 iPhone Duo 的 iOS SDK 和模拟器支持);General,Known Issues,187146039,有引用。 ↩