不管从零做还是旧 App 改,都能丢给 AI 的 iPhone Duo 开发指南
文章目录
大家好,我是 Kyrie。
苹果发布会之后,这些天我和我们软件开发团队,把苹果官方的设计指南、开发适配指南、六个开发者视频和相关接口文档都精读和研究完了,同时也开启了诸多软件的适配工作。
如果你有开发苹果软件相关的产品,不妨趁快打出“适配 iPhone DUO”的名号,抓一波热点。过程中可以参考我这篇介绍。
设想一个学习笔记 App。外屏上它是普通的笔记列表,跟现在的 iPhone 一样。把手机掰开,系统工具栏跑到屏幕侧边竖起来了,App 自己画的那排底部按钮却还贴在屏幕底端。其中一颗按钮正好骑在屏幕中间的弯折处。眼睛看得到它,手指按上去却不容易点中。
苹果在 2026 年 9 月 9 日发布了 iPhone Duo。机器定在 10 月 16 日开放预订,10 月 23 日在首批 70 多个国家和地区发售,起价 1999 美元。配套的 Xcode 27.1 beta 带着 iOS 27.1 SDK 和模拟器,直到 9 月 18 日才推送到开发者手里。离预订只剩不到一个月。
这篇文章可以给亲手写代码的开发者做参考,也可以给用 AI 做开发的朋友们直接使用,文末有一份能复制给 AI 使用的作业指导书。
展开以后,笔记旁边能放什么
合上手机时,学习笔记 App 在一块长条外屏里运行。想读参考资料,整个屏幕就被英文原著占满。想记两句感悟,必须点开弹层或者跳进新页面,原著立刻被盖在底下。
展开以后,内屏长宽比约为 1.42,接近 A4 纸。如果在这块屏幕上播放十六比九的视频,会留出大约五分之一的黑边。这块屏刚好能并排放下原文和笔记。左边保持原文阅读,右边停留在笔记编辑界面。眼睛看一句,手指在旁边敲一句。
苹果自己的备忘录展开后,左边列表、右边正文
系统给 App 的空间类型也变了。外屏是紧凑宽度,也就是 size class 里的 compact,一般一次只放一列。内屏展开后是常规宽度,regular,可以同时放下侧边栏和主工作区。
你的项目现在能做到哪一步
编译用的 SDK 不同,展开以后的画面就不一样。
App 在 Duo 上怎么显示,看编译时用的 SDK,不看手机里装着哪个系统。哪怕用户拿的是预装 iOS 27.1 的零售版真机,打开一个用旧 SDK 打包的旧 App,系统仍然让它走兼容显示,两边留黑。
同一个 App 用三档 SDK 编译,从上到下依次是两边留黑、铺开、铺满且工具栏竖到侧边
9to5Mac 的分析是,工程一切到 iOS 27 SDK,就没有兼容模式可以躲液态玻璃了。团队来不及重做整套视觉,就继续用旧 SDK 出包。
有人会建议让 AI 用 if #available 写一段判断,在新系统上调用新接口,旧系统上继续走老逻辑。这种写法在这次适配里会直接碰壁。iOS 27.1 引入了 ReservedRegion 和 ArrangementView 两个专门处理折叠排版的新类型,目前它们还处于 Beta 阶段。如果你手头的 Xcode 还停留在老版本,编译器在解析代码时根本不认识这两个词。if #available 只在运行时分流,包一层也编不过。
新项目
第一版先做一条能用的线。以学习笔记为例,打开一份资料,读,记下笔记,退出再进来,内容还在。先把这条单线在普通手机上跑顺,再去添加展开后的双列左右并排。
数据保存从第一天就要写好。开合时界面会拆掉重来。存盘不稳的话,正在写的字极易丢失。
已有的 App
如果是给已经在运营的老项目做适配,第一步应当排查代码里写死的屏幕假设。过去很多代码默认屏幕永远是固定尺寸的长方形。你可以在工程根目录下用检索工具把这些旧写法找出来。
rg -n --glob '*.swift' 'UIScreen\.main|userInterfaceIdiom|UIDevice\.current\.orientation' .
rg -n --glob '*.swift' 'ignoresSafeArea|safeAreaInsets|UIToolbar\(' .
搜到了先别急着改,看一眼上下文。头像写成四十点,留着。阅读器或编辑器的宽度要是绑死了屏幕宽度,再改。
很多老项目还开启了 UIRequiresFullScreen 配置。这个开关挡不住手机折叠开合带来的尺寸剧烈变化。不过老项目设置它往往有特殊的原因,改之前先问原来维护的人。
老项目先改学习笔记的主编辑页。
开发者 Blake Crosley 用探针实测发现,当 App 运行在展开的内屏上时,UIScreen.main.bounds 报的仍然是外屏的 466×678 点。拿这个值去算编辑器宽度的老代码,排出来的是一块外屏大小的界面,内屏剩下的地方就空着。这个页面的宽度改由当前窗口提供,展开后才不会只画出一块外屏那么大。
Xcode 27.1 里有个官方技能,叫 App Resizability。它在 WWDC 上叫应用现代化技能,能处理 SwiftUI,也覆盖 iPhone Duo。它能帮你找出 UIScreen.main 这类旧写法,改成按当前环境布局。
原文和笔记同放
双手横握的时候,两栏并排放得下。用户顺手把手机竖过来,横向宽度迅速收窄,两列内容立刻挤在一起。把手机折起成九十度放在桌面上,立起来的半截屏幕适合当成支架阅读,贴在桌面的半截屏幕刚好放平用来打字。这时候只要在笔记里轻轻点一下,系统软键盘升起,瞬间又吃掉半截屏幕的高度。
姿态一多,代码里容易堆出好几套判断。横着一套,竖着一套,半折一套,键盘弹起再一套。再算上大字体和分屏,漏一种,控件就被压扁,或者空出一大块。
并排和叠放
先分清内容之间的关系。从书库点进一本书、再从书里返回,是一层一层的导航,交给 NavigationStack。书里的原文和旁边的笔记是同一件事里的两块内容,用户要的是一边对照一边写,不用再 push 一个新页面。这两块怎么摆,交给专门管排列的容器。
苹果在 iOS 27.1 引入了专门的 ArrangementView,在 UIKit 对应的类是 UIArrangementViewController。它只负责把两块内容按照环境安排在屏幕上,不管页面怎么进退。
NavigationStack {
ArrangementView {
ReadingPane(document: document)
} secondary: {
NotesPane(document: document)
}
.arrangementViewStyle(.split)
}
外层导航栈管页面进退,中间的排列容器管怎么摆,最里面是两块面板。ReadingPane 和 NotesPane 只管画好自己的段落排版和输入框,完全不需要知道外面的屏幕折到了多少度。
split 把两块内容并排摆开,Primary 和 Secondary 各占一边
ArrangementView 主要有两种摆法,split 和 overlay。差别是两块内容能不能前后叠。
原文和笔记必须选择 split。
overlay 适合前后叠放的界面组合,最典型的例子是视频播放器与它的浮层控制面板。当用户把手机半折放在桌上时,ArrangementView 知道这两层是叠放关系,折起来时可以把它们拆开摆,画面在一边,控制在另一边。重新展平,两层又叠回去。
半折立在桌上时,影片在上半边,播放控制拆到下半边
如果误把原文和笔记配成了 overlay,平整展开时,笔记输入框会直接叠在原文正前方,浮在最上面。用户在键盘上敲字,输入面板刚好把底下正在精读的参考资料挡得严严实实。
窄了以后
如果只允许横向拆分,宽度够的时候没问题。竖着拿,或者拉成窄条分屏,宽度不够排两栏。这时容器可能只显示排在第一位的主要面板 ReadingPane,次要面板 NotesPane 就从屏幕上消失了。
双栏退化成单栏时,界面上必须给用户保留一个随时切换的入口,可以在导航工具栏放一个带有「原文 / 笔记」的分段选择器,或者在角落放一个醒目的切换按钮。
苹果演示里用 split.axes(.horizontal) 把排列限制为横向拆分
设想用户正展开手机在笔记里飞快输入,双手在键盘上敲得正起劲。这时用户顺手合上手机,或者在多任务系统里把当前窗口拖成窄条。如果容器没有特意处理面板的选择记录,很多默认逻辑在尺寸变化后,会自动重置回排在首位的主要面板。用户眼前正在编辑的半截句子不见了,屏幕上换成了原文。
所以单栏时显示哪一块,要看用户最后在用哪一块。刚才在打字,折叠收拢后就继续留给笔记面板;刚才在翻看原文,折叠后就显示原文。只有第一次从书库点开这篇资料,才先给出原文。
有些工程为了防止在小屏幕上出现内容截断,习惯在外层套上一个 ScrollView,把整个 ArrangementView 硬生生包在里面。左边原文和右边笔记本来各自滚动。外层再套一个大 ScrollView,两块就被拉成一条超长内容。右边笔记把键盘弹起来时,外层滚动视图会把整块排列往上顶。用户在右边敲进一个标点,左边的参考句跟着整块容器被推出了屏幕。让排列容器占满它拿到的空间,滚动交给两个面板各自处理,就没有这个问题。
两块面板并排摆开时,宽度的切分比例不需要死板地各占一半。书籍排版有一套讲究的阅读行宽。行宽太宽,眼睛从行尾换到行首容易跳行;行太窄,一行只能放下两三个词。笔记面板则要放得下输入框和几个常用按钮。先给原文一行读着舒服的宽度,剩下的给笔记,比五五平分更合适。
遇到大字体时,如果特大字号下放不下两栏,就退成单栏。
界面怎么摆,完全取决于当前这块窗口拿到了多少空间,不需要在代码里判断手里的机器是不是 iPhone Duo。哪怕机器在桌上展开得平平整整,只要用户拉出侧边分屏,分配给当前 App 的依然是一个紧凑宽度的长条窗口。如果代码里写死「只要机型是 Duo 就排双栏」,分屏下的工作区就会挤得没法用。以后再出别的尺寸,这段代码也不用改。
ArrangementView 是 iOS 27.1 才引入的新特性。不必为了适配一台新机器,就把整个工程的最低部署版本直接拔高到 27.1。
新组件只在支持它的系统上激活。老版本系统上继续使用现有的标准布局容器,例如用条件视图或者传统的 HStack 处理左右排列。地方够就并排,不够就留当前这块。
换掉的只是外层管位置的那层容器。新设备上用新排法,老设备上照旧,两边读写的是同一份数据。
底部按钮为什么跑到了侧边
把学习笔记 App 展开,系统的标准工具栏离开了底端,立在侧边。App 自己画的那排按钮还在底端。
同一个健身 App,左边普通 iPhone 标签栏在底部,右边 Duo 外屏上竖到了侧边
很多人的第一反应,是顺着屏幕宽度写一段判断,展开时把底部这排按钮搬到屏幕右侧。照这个做,马上会撞上一串例外。
当用户把展开后的内屏竖着拿起来读笔记,标准的系统工具栏依然横排在底端。
设想用户左右分屏的场景。左边开着学习笔记,右边开着英文词典。如果两个 App 都自作主张把按钮画在屏幕右侧,两个工具栏就会在屏幕中央碰头,甚至跟中间的分屏拖拽条挤在一起。实际的规则是系统把它们各自推向外侧,左边那个 App 的工具栏停在最左边,右边那个 App 的工具栏才留在最右边。
两个 App 左右分屏时,控制区各自靠外侧
如果在学习笔记里采用了导航分栏结构,侧边竖排也只发生在详情那一列。左侧的目录和文章列表保持原样,不跟着竖排。
按钮放哪,系统看当前的姿态、分屏位置和视图层级来定。你要说清有哪些操作、每个按钮什么意思、哪个更重要。
系统自己的工具栏会换边,自己画的那排钉在底端。
很多老工程在界面底部放置按钮时,习惯在控制器里自己创建一个 UIToolbar 实例,通过布局约束或者固定坐标贴在屏幕底端。在普通的直板 iPhone 上,这套写法跑了很多年都很稳定。系统只把这种自己画的栏当成一块普通内容,不知道它是操作栏,开合时也就不会替它换到侧边。
栏要交给导航控制器,或者用 SwiftUI 的 toolbar 挂到导航上,系统才会在开合时把它算进侧边那一排。阅读页和编辑页先换回系统标准栏。
beta 模拟器里工具栏不会竖起来,一直横在顶上,这项不能用来判断代码对不对。
图标和文字两样都要给。侧边很窄,系统只画图标。按钮挤进溢出菜单时,菜单里用的是整句文字。VoiceOver 念的也是这句文字。
SwiftUI 里给按钮套一个 Label,图标和文字就都有了。
Button(action: saveNotes) {
Label("立即保存", systemImage: "square.and.arrow.down")
}
界面上也有几种保留横排更合理的特殊情况。
「购物车 ¥238」这样的按钮要留住价格,不能被系统压缩成单纯的购物篮图标。
选择和完成要留住会变的文字,文字变动直接提醒当前的编辑模式。
贴着键盘的附件栏继续横排,用户打字时手指集中在屏幕下半段,附件栏继续贴着键盘横向排开。
备忘录在 Duo 外屏上,返回和完成竖到了侧边,格式附件栏还贴在键盘上方
如果有团队自己封装了独特的交互控件,想要让它参与侧向排列,先在设计稿上确认它竖立起来的样子确实好用,再去代码里设置垂直偏好属性 verticalPreferred。这只是告诉系统你希望它竖着放。系统不会把一根横向拖动的滑杆变成好用的竖条。
一旦界面转为侧边竖排,侧边往往要同时容纳顶部的导航按钮、主操作栏以及底部的分段标签。如果此时用户调出键盘输入文字,或者屏幕角落浮着画中画视频,侧边的纵向空间会被进一步挤压。
地方不够时,谁先收进溢出菜单,看你给操作排的优先级。
资料库列表页里,找资料、打开资料优先留在外面。笔记编辑页里,保存和切换面板最后收,外观设置可以先收进溢出菜单。
别把所有操作都设成最高优先级,那就等于没排。收进溢出菜单的每一项,都得点了真能执行。
说到压缩,看苹果的开发者视频时我注意到一件事。字幕里说的接口叫 toolbarCompressionBehavior,照着字幕让 AI 写代码,编译器会直接报错。这个接口在 SDK 里的真名是 toolbarVerticalCompressionBehavior。真名以 SDK 为准。
正文照旧,录音按钮换个位置
设想学习笔记展开后的样子。左边是参考资料,右边是正在编辑的笔记。正文上层浮着一颗圆形的录音按钮,方便随手记录语音想法。笔记中间还嵌着一张实验记录照片,照片下方附着一排操作菜单,用来标注重点和裁剪画面。
用户把手机上半截往前推,让两块屏幕形成一个夹角,像一台小掌上电脑那样立在桌面上。这一折,屏幕中间出现了一条物理弯折区。原本浮在右下方的录音按钮,位置正好压在这条折痕上。
半折之后录音按钮放在左半边,避开了中间的折痕
控件被折痕挡住时,不能只看 safe area。safe area 是四边的安全范围。摄像头、底部横条、侧边工具栏都算在里面,但管不到屏幕正中间。
屏幕中央的排版问题交给了 reserved regions 这个新机制。苹果在接口里定义了两类区域。一类是 division,也就是屏幕处于弯折姿态时出现的折痕区。另一类是 occlusion,用来标记摄像头等硬件带来的物理遮挡。外屏的前置摄像头物理存在,所以外屏的 occlusion 始终生效。内屏的前置摄像头只有在实际开启工作时才算作遮挡,折痕跟着机身开合出现。手机完全展平时,内屏中间没有折痕。不要在那儿永久留一条空缝。
内屏上的两类区域,中间一条是弯折区,右上角是摄像头区
正文和长列表照常滚。这类内容不需要躲开弯折区。用户读到第七段,如果整篇文章因为机身半折突然跳到右半边,阅读就被打断了。
常驻屏幕底部的搜索栏、浮动录音按钮、侧边操作球都需要主动避让。折痕一旦出现,就把录音按钮挪到折痕一侧的开阔位置。
贴着照片的菜单跟着照片一起动。如果因为折痕正好划过照片附近,就把菜单挪到半个屏幕之外,用户就不知道这排按钮管的是哪张图。遇到这种情况,让照片和菜单一起整体微调。
半折时,左边天气的面板滑到一侧避开折痕,右边照片和它的菜单一起留在上半边
排版结构本身也能减少避让的麻烦。要放很多资料卡片时,网格选用两列或四列这类偶数列,卡片就会自然分列在折痕两侧。很多次级操作也可以收进上下文菜单。把加入笔记这类动作放进长按呼出的气泡里,界面上就少了一颗浮动按钮。
背景色铺满整块屏幕。正文、列表和按钮留在安全区内。不少老工程的问题,是从最外层随手挂的一行 ignoresSafeArea 开始的。背景是铺满了,里面的按钮也跟着出了安全区。
间距别再外扩。系统返回的保留区域 frame,内部已经包含了苹果推荐的安全边距。拿到这个矩形之后,再手动加一圈边距,避让区就会放大。弯折区原本只占中间一条窄缝,二次外扩却在屏幕中央制造出一大块空白禁区,旁边的内容也被挤开。
视图坐标系容易错位。查询保留区域拿到的 frame,采用发起查询那个视图的局部坐标。录音按钮通常装在带有内边距或局部偏移量的子视图里。两个矩形在调试器里打印出来的数值看起来差不多,实际位置却完全对不上。两个矩形先换到同一个坐标系里再比。
计算避让时,还要按照按钮的完整点击热区来算。图形图标可能只有二十点宽,但在人机交互规范里,手指可点的有效范围通常需要达到四十四点以上。如果只拿二十点的小图标去计算重叠,手指按在热区边缘依然会压在折痕上。
我在查阅苹果官方资料时发现,官方文档和技术视频的说法出现了矛盾。文档写明查询保留区域会返回全部区域,无论当前是否处于活跃状态。开发者视频里的演讲者却表示,默认只会给当前活跃的区域。查询时带上 includeInactive 获取完整数据,在决定是否搬动按钮时,再用 isActive 属性做过滤。当用户把手机平放在桌上完全展开时,弯折区处于非活跃状态,它的宽度读出来是零,按钮自然保持在原有位置。
let divisions = geometry.reservedRegions(
kind: .division,
options: .includeInactive
)
let activeDivisions = divisions.filter { $0.isActive }
在阿拉伯语等从右往左阅读的语言里,界面的流动方向是反过来的。设备上的物理前置摄像头和物理折痕,并不会因为系统语言改变而对调物理方位。SwiftUI 在处理从右往左的语言时,默认会对布局坐标做镜像处理。避让交给系统布局去对齐。别自己把横坐标翻一次,不然按钮会跑到另一侧。
关于屏幕边缘,网上流传一种说法,声称 iPhone Duo 的横向 safe area 一直为零。我顺着找了一下来源,最接近的是苹果开发者论坛里的一则帖子。那位发帖者当时只测了一种特定场景,即 App 处于左右分屏的左侧窗口,测出的左侧边距确实是零。但开发者 Blake Crosley 用探针实测全屏展开状态,内屏左右两侧的安全边距各自都是三十四点。
也有人尝试拿三倍屏幕缩放比例去推算物理像素断点,这同样只是估算。边距每次用当前视图的 safeAreaInsets 问系统要。即使是本文里列出的三十四点,也不要直接写死在工程配置文件里。
目前的 beta 模拟器里 reservedRegions 一直是空的,避让算法只能先自己塞一个测试矩形来验。
合上手机,依旧可以编辑
设想用户完全展开手机,对照左侧资料在右侧笔记里写心得。一句话敲到一半,用户合上手机。外屏亮起时,笔记停在屏幕中央,原本停在文字中间的光标却跳回了整篇笔记开头。在另一些代码实现里,刚才敲进去的半句话会直接消失,甚至合上后退回更早的历史草稿版本。
SwiftUI 靠视图的身份记住光标、选区和键盘。很多开发者在编写双屏适配时,习惯在顶层写下一段条件判断。遇到内屏的常规宽度就渲染一套宽屏双列结构,遇到外屏的紧凑宽度就切换成另一套单列结构,并在两套结构里各自独立放置一个文本编辑器。
写在不同条件分支里的两个编辑器,是两个互不相干的视图。手机从展开到合上的过程里,屏幕宽度剧烈收缩,系统会销毁宽屏里的旧编辑器,同时在窄屏里重新构造一个新的编辑器。旧视图被销毁时,附带在它身上的文字选区、光标停留点和软键盘激活状态会被系统一起清掉。
在 SwiftUI 里如果给视图附加了 .id 修饰符,尺寸一变就换一个新的 .id,系统会把它当成新视图,光标和选区都丢掉。在传统 UIKit 里也会遇到相同的状况,如果在尺寸改变的回调方法里重新实例化整个视图控制器,通常会顺带把控制器持有的数据模型一起重置,刚刚写下的草稿就会丢失。
外层可以从双列改成单栏,文本编辑器得还是同一个。
折叠时如果每次尺寸变化都去抢焦点,键盘会一下弹起、一下收回。等排版停住,用户自己点了输入框,再把键盘打开。
别把这些变量都放进一个全局对象,两个窗口会互相干扰。下面按能活多久分成四类。
合上、单栏、两栏三种状态,打开的笔记都是一样的
记录阅读进度时,如果习惯保存当前滚动条的绝对像素偏移量,在折叠屏上就会失效。内屏与外屏的横向阅读宽度完全不同,文字折行重新排布后,离顶部 842 点的地方,到外屏已经是后面的段落。用户把字号调大,会偏得更厉害。
文档第一次导入时生成稳定的段落编号,滚动时记下屏幕上现在是哪一段。只要文档内容保持稳定,后续界面无论经历多少次重绘,都一直用这组编号。每次重绘都重新随机编号的话,合上再找,那段已经对不上。
FocusState 只知道键盘停在哪个输入框,不知道光标在第几个字后面。要留住光标,得记下选区在字符串里的范围。
存盘交给 AI 写,常会写成 try?。失败被吞掉,界面仍标成已保存。用户一退出或一合上,刚打的字就没了。失败时字留在内存里,界面写明没存上,让人能再试一次。
private func save() {
do {
try modelContext.save()
saveError = nil
didSave = true
} catch {
didSave = false
saveError = "暂时无法保存,请重试。"
}
}
还有一种丢字,是界面每次出现都从文件里重读全文。手机一合,排版重来,这次重读把磁盘上的旧文件盖回来,刚打的、还没存进去的字被盖掉。第一次创建、用户手动刷新、界面重新排版,这三件事要分开写。排版只改外观,不去碰数据。
展开手机,在笔记里把光标停在一段话中间,打几个新字,接着合上。外屏上继续打,看刚才的字还在不在原地,新字是不是接在光标后面。
两块屏还能玩出什么
如果你的 App 没有影像拍摄需求,也没有计划支持多窗口,这一节完全可以选读。
先看拍照时两块屏各显示什么。拿着手机的人在内屏看取景器,站在对面的被摄对象看外屏上的倒计时、提词文本或者构图预览。苹果通过 CameraCaptureAccessory 提供了这项能力。外屏要显示这些辅助内容,四个条件必须同时满足,App 处于前台运行状态,内屏全屏显示相机界面,有一个正在进行的拍摄会话,系统当前的可用状态 availability 允许开启外屏配件。
苹果演示的提词器,拍摄时提词显示在外屏
在保存状态时,要把系统的可用状态与用户的开启意愿分开存储。系统允不允许由 availability 表示,用户想不想开由 isEnabled 表示。把这两个值混在一个变量里,系统一旦暂时不允许,用户的选择就被一起抹掉了。停止录制、按快门留在内屏上。外屏随时可能因为用户合上手机或者切回主屏幕而熄灭,外屏没了,用户照样能在内屏把这段拍完。
演示代码里,用户开关 isEnabled 和系统可用状态 isAvailable 是两个值,系统不允许时开关变灰
以前的代码习惯把 .front 当成朝着用户的那颗摄像头。Duo 配备了两块屏幕和两颗前置摄像头。用户换了一块屏幕看界面,正对用户的摄像头就换成了另外一颗。
系统提供了一颗虚拟前摄。它会跟着用户换屏,在内外两颗物理前摄之间自动切换,用起来省事。它只有两颗摄像头都有的功能。某一颗独有的功能用不上。
想用某一颗独有的功能,就要换用 AVCaptureDeviceDirectionCoordinator。这个协调器以某一个具体的视图作为物理参照,告诉你摄像头相对这个视图朝向哪边。内屏和外屏各用一个独立的协调器。协调器工作在主线程,拿到描述信息之后交给负责拍摄的 actor 去解析具体的设备对象。协调器第一次回调是异步的,刚进界面可能还没有值。
外屏和内屏各有一颗前摄,朝向相反
核对取景界面的画面镜像、屏幕横竖旋转时的方向对齐、写入相册的文件真实方向。取景器里的画面看起来是正的,最终保存到磁盘里的视频文件依然可能上下颠倒。
SwiftUI 里用 onHingeChange,UIKit 里用 UIHingeInteraction,都能拿到连续的开合角度。
开合角度控制吉他的弯音
开合角度是连续的,可以拿来做跟着角度走的效果,比如弯音。界面排版仍交给前面的排列容器和保留区域。别拿某个角度数值当换界面的开关。没有铰链的直板手机上,这些效果直接关掉。
多屏在系统里对应着三种形态。
和别的 App 分屏。两个独立的软件各占半边屏幕,系统分给你一块紧凑宽度,界面排成一列就行。
同一个 App 开两个窗口。用户可以在左侧窗口翻开参考资料,在右侧窗口编辑笔记。两个窗口各管各的文档。系统规定新窗口只能在展开后的内屏里创建,外屏无法开启新窗口。向系统请求开新窗口时,请求可能失败,要写上失败怎么办。
内外屏同时亮。这只在相机拍摄这类特定能力下开放,普通页面不能自己往外屏画辅助画面。
Flutter 的 MediaQuery.displayFeatures 在 Duo 上不管怎么折都返回空列表。React Native 官方目前也没有提供适配折痕的稳定接口。跨平台项目先做宽了并排、窄了单栏。
如何确定改造完成
一个页面展开时正常,合上时也正常,中间切换那一下仍然可能丢草稿。两张截图都好看,截图之间发生了什么,截图里看不到。
验收分三层做。
第一层是静态代码审计。在代码里检索,确认没有写死屏幕尺寸,没有调用废弃的方向接口,保存失败没有被 try? 吞掉。
第二层是构建与模拟器运行。在 Xcode 里完成编译,在不同尺寸的模拟器里验证页面跳转与双栏排布。在 9 月 18 日推送的 Xcode 27.1 beta 模拟器里,reservedRegions 接口在所有折叠姿态下返回的都是空数组,系统标准工具栏也始终横排在顶部,没有竖立起来过。Blake Crosley 在探针实测中测出内屏展开尺寸为 669×951 点,模拟器汇报的 UIScreen.main 却依然停留在外屏的 466×678 点。
第三层是真机交互验证。只有拿着真机在手里实际开合,才能看清弯折区的按钮好不好点、工具栏什么时候竖到侧边、外屏辅助画面什么时候出现。
主流程要是 OK 了,一次加一个变化。特大字号、阿拉伯语、分屏在左、分屏在右、半折,各跑一遍。哪一次坏了,就是刚加上的环节引起的。
有两种假通过:
一个是分屏测试只测了一边。不少人把 App 拖到屏幕左侧验证,看到排版正常就以为通过。当用户把 App 拖到屏幕右侧时,工具栏会移到最右边,系统的安全边距也会随之对调。左边看起来正常,右边可能发生遮挡。
另一个是把大字体缩回去。当系统字号调大后,界面上的文字容易撑出屏幕。让 AI 协助处理时,AI 经常顺手加上一行代码,把字号缩放比例压小。字是缩回去了,用户自己的设置也被取消了。
附录 AI 作业指导书
下面五个代码块一起复制给 Claude Code 或 Codex 或 其它开发 Agent,它可以前后对照着用。基于此进行你的软件开发或适配,会规范很多,也可以相应有个合适的工作流程。
一 总入口与环境核对
1. 目标与问询
你在编码前先向用户确认两件事:当前是新项目还是已有 App;优先做哪个核心流程(如打开资料、阅读、写笔记、保存)。若用户未说明,提出一两个问题问清楚再开工。
2. 环境检测与 SDK 档位
运行 xcodebuild -version、xcodebuild -showsdks 与 xcrun simctl list runtimes。向用户汇报 Xcode 版本、可用 SDK、最低部署版本以及有无 iPhone Duo 模拟器。
说明当前工程所属档位:
- 早于 iOS 27 SDK:走兼容显示,保持经典比例,两边留黑。
- iOS 27 SDK:内屏可用空间扩大,能延伸至状态栏周边。
- iOS 27.1 SDK:内容延伸至边缘,标准导航栏与工具栏参与侧边竖排。
3. 声明约束与推进
调用新 API 前,先在 SDK 中确认真实声明。例如工具栏压缩属性以 SDK 里的 toolbarVerticalCompressionBehavior 为准;查不到就把该功能列为「待 SDK 支持」,其余照常推进。if #available 只管运行时分支,新类型要求 SDK 本身认识,当前 SDK 不认识的类型,相关代码整块列入「待 SDK 支持」。若有 App Resizability skill,建议用户开启辅助。
后续按「一次只做一个能运行的阶段」推进。
4. 交付物
环境核对与档位报告、本阶段修改文件、已验证项及待验证项。
二 新项目开工
1. 范围与持久化
第一版只做一条完整流程,先在普通手机尺寸跑通:列表打开资料、阅读、写笔记、保存,退出再进内容依然在。
使用 SwiftData 本地持久化:
- 资料有稳定 ID;段落有导入时生成一次的稳定 ID;笔记属于资料模型。
- 状态分四类:数据模型(持久化实体)、窗口导航(页面路由)、工作区(当前面板与段落阅读位置)、编辑界面(FocusState 管焦点,光标选区另行保存)。
2. 架构与容错
- 导航用 NavigationStack。双面板排布放在单独布局层:支持的系统上启用 ArrangementView,旧系统用普通容器,业务代码共用一套。
- 保存显式 do/catch。失败时保留用户输入并提供重试,禁止用 try? 吞掉错误。
3. 阶段顺序与交付物
按顺序推进:能打开 → 能保存 → 能改变排列 → 阅读和输入能连续 → 自定义控件能避让 → 完整流程反复跑。
每阶段交付物:可运行源码、改动说明、状态分层记录与验证结果。
三 已有 App 审计
1. 审计原则与排查项
在修改前执行只读审计,不改动业务代码,先交审计表。
排查源码、Storyboard、XIB 与工程配置中的假设:
- 搜寻硬编码屏幕属性:UIScreen.main、userInterfaceIdiom、设备方向 UIDevice.current.orientation。
- 搜寻边距与容器:ignoresSafeArea、safeAreaInsets、自建 UIToolbar。
- 搜寻配置与保存:UIRequiresFullScreen、带有 try? 的保存调用。
2. 审计条目与分类
每个检出项写清:文件位置与行号、原来的假设、什么情况出问题、用户看到什么、最小改法。
条目归为三类:
- 确定的问题:违反自适应规则、展开后必定失真的硬编码。
- 需要运行验证的风险:特定姿态或分屏时可能异常的代码。
- 合理用法:符合业务的固定尺寸(如头像固定 40 点),不用改。
3. 改动建议与交付物
挑出最该先改的 3 项和一个核心页面,说明哪些当前 SDK 就能做,哪些需等待新 SDK。
交付物:Markdown 格式《适配审计表》与首批改动建议。
四 单页实现
1. 布局与结构层次
界面按当前容器尺寸、size class 与 safe area 自适应,不绑定机型或分辨率。
- 层次规范:NavigationStack 在外、ArrangementView 在中、内容在里;排列外层不包 ScrollView。
- 同时看的内容用 split;前后叠放的内容用 overlay。
- 若排列只允许横向拆分,给被收起的内容保留入口;窄屏时显示用户最后操作的面板。
2. 工具栏与避让
- 用导航容器管理的标准栏。按钮用 Label 给图标和文字;内容带信息的按钮、状态切换按钮与键盘附件栏保留横排。按组设优先级,确保溢出菜单里的动作能调用。
- 背景可铺满,前景留在安全区内。左右边距分别从 safeAreaInsets 两侧读取。
- 连续正文照常滚动,固定浮层要挪,菜单跟着内容走;动作优先放进上下文菜单。
- 查询写 includeInactive,避让用 isActive 过滤。frame 已含 margins,比较前统一坐标空间,按完整点击范围判断。RTL 依赖系统镜像,不手动再翻 x 坐标。
3. 状态与分支
- 同一编辑器跨布局保持身份,草稿与光标不丢。阅读位置存段落 ID,页面出现时不覆盖用户数据。
- 相机:两块屏各一个 AVCaptureDeviceDirectionCoordinator。系统 availability 与用户开关 isEnabled 分开存,停止按钮留在主界面。
- 铰链角度 onHingeChange 仅做连续特效,禁止按固定角度切换排布。新窗口只在内屏创建并处理失败。
4. 交付物
单页重构源码、排布与状态说明、区域避让测试记录。
五 验收与交付
1. 三层记录体系
验收分三层记录:静态审计、构建与模拟器运行、真机交互。
每项写环境、步骤、预期、实际、证据、状态(通过 / 失败 / 未执行)。没有运行过的禁止标为通过。
说明当前 beta 模拟器限制:弯折区域为空、工具栏不竖排。这两项标为待真机验证。
2. 核心流程演练脚本
走查主流程:
打开资料 A 滚到中间 → 笔记输入两行且光标放第二行中间 → 键盘开着时开合设备 → 继续输入确认草稿与焦点完整 → 切回原文看阅读位置是否保持 → 保存 → 打开资料 B 确认没串 → 退出 App 再进确认数据完整恢复。
3. 变量矩阵测试
在主流程基础上,一次加一个变量:
- 大字体测试:放不下时调整排布,保留用户字号,禁止随意缩小字体。
- RTL 语言。
- 分屏左侧与右侧。
- 半折状态。
- 保存失败重试。
4. 沉淀文档与交付物
在项目里建 docs/iphone-duo.md,记录本项目采用的规则、SDK 版本、例外和待验证项。
若已有 AGENTS.md 或 CLAUDE.md,在文末加一行指向它,不覆盖原内容。
交付物:分层验收表、变量测试记录、新建的 docs/iphone-duo.md。
关于作者
Kyrie — 前国内大厂 R&D ,现居曼谷,做中国科技企业出海 BD。持续分享出海一线真实记录、AI 在业务里的实战用法,偶尔也聊聊美股投资和国外生活。
























