9 月 9 日,Apple 发布了 iPhone Duo——第一款折叠屏 iPhone。用户在讨论 1999 美元的售价和屏下摄像头,而留给我们开发者的,是六场 Tech Talk、一页新的 HIG,以及月底前发布 Xcode 27.1 beta 的承诺。我把六场演讲全部看完,在这篇文章里整理出几乎每个应用都绕不开的改动——代码同时给出 Swift 和 Objective-C 版本,因为新 API 的 UIKit 部分两种语言都能调用,而世界上的 legacy 代码库比我们愿意相信的多得多。
先声明一点关于可信度的事:设备 10 月 23 日才上市,写这篇文章时 Xcode 27.1 还处于 "coming later this month" 状态。下面所有签名都来自官方 Tech Talk,但在与 SDK 头文件核对之前,它们只是"API 的形态",不保证逐字符准确。
不是新 idiom,而是尺寸的连续谱#
Apple 几乎在每场演讲里都反复强调:iPhone Duo 不是需要单独写一套界面的"新设备类别"。它仍然是 iPhone,只是可用尺寸的范围变宽了。
具体来说:外屏(5.4 英寸)表现和普通 iPhone 一样——compact 宽度、常规的方向行为。内屏(7.6 英寸)则几乎是一台 iPad:任何方向下都是 regular × regular。而下面这个细节会弄坏最多的应用:内屏无视 supportedInterfaceOrientations。锁定竖屏的应用在内屏上照样会旋转,没有任何办法禁止。
| 配置 | Horizontal | Vertical |
|---|---|---|
| 外屏,竖屏 | compact | regular |
| 外屏,横屏 | compact | compact |
| 内屏,任意方向 | regular | regular |
由此得出核心规则:所有按方向、按设备 idiom 分支的代码(userInterfaceIdiom、"如果是 iPad 就两栏")都会变成缺陷。唯一可以依据的分支条件是 size classes:
// SwiftUI
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
if hSizeClass == .regular {
TwoColumnLayout()
} else {
SingleColumnLayout()
}
}// Objective-C: same question via the trait collection
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
BOOL isWide = self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular;
[self applyLayoutForWideMode:isWide];
}如果代码库里有一段逻辑的含义是"如果这是 iPhone Duo"——这就是一个信号:停下来,把它重新表述成"此刻到底有多少可用空间"。
三档 SDK:重新编译能换来什么#
应用在 Duo 上的行为取决于它是用哪个 SDK 编译的。一共三档:
- iOS 27 之前的 SDK。 不改也能跑,但是"信箱模式":合上时内容够不到状态栏和摄像头区域,展开时保持惯常尺寸、四周留边。
- iOS 27 SDK。 内屏上内容扩展到状态栏区域。
- iOS 27.1 SDK(Xcode 27.1)。 完整 edge-to-edge,系统栏自动切换为竖排,reserved regions 和 arrangements 等 API 全部解锁。
用 27.1 重新编译,是所有改动里成本最低、收益最大的一项。不做这一步,后面的适配根本无从谈起。
猎杀写死的假设#
折叠屏移植的每一个 bug,背后都是代码里某个被写死的假设。以下三个现在就值得 grep 一遍。
UIScreen.main 已死。 在双屏设备上,"主屏幕"是个含义不明的概念,Apple 明说这个 API 正走向废弃。替代方案:
// Before
let scale = UIScreen.main.scale
// After
let scale = traitCollection.displayScale
// If you really need the screen
let screen = view.window?.windowScene?.screen// Objective-C
CGFloat scale = self.traitCollection.displayScale;
UIScreen *screen = self.view.window.windowScene.screen;对称的 inset 计算。 Duo 上的 safe area 经常是非对称的:竖排系统栏和 Dynamic Island 位于某一侧,而具体是哪一侧,取决于设备姿态以及应用处于 Split View 的哪一半。width - insets.left * 2 这种经典写法现在会算错:
// ❌ assumes symmetry
let width = view.bounds.width - view.safeAreaInsets.left * 2
// ✅ each edge on its own
let width = view.bounds.inset(by: view.safeAreaInsets).width// Objective-C
CGRect contentFrame = UIEdgeInsetsInsetRect(self.view.bounds,
self.view.safeAreaInsets);硬编码宽度。 "设备宽度 → 布局"的映射表、width > 390 之类的断点——在一台用户抬手就能改变宽度的设备上,这些全都会散架。
竖排工具栏:免费,但只给系统容器#
Duo 的两块屏都比普通 iPhone 更宽更矮,所以系统控件搬到了侧边:状态栏、Dynamic Island(现在是竖向的)、toolbar 和 tab bar。应用可以自动获得这一切——前提有两个:用 iOS 27.1 SDK 编译,并且使用系统级的栏容器。
手动加进视图层级的自定义 UIToolbar、UINavigationBar、UITabBar 完全不参与竖排布局。参与的只有 UINavigationController、UITabBarController 和 SwiftUI 的 .toolbar。如果你有一条自己拼的按钮栏——它就是第一个迁移对象。
新 API 里最有用的一段:
// SwiftUI: pin the primary action, send the rest to overflow
.toolbar {
ToolbarItem(placement: .topBarPinnedTrailing) {
Button("Post", action: post)
}
ToolbarItem(placement: .cancellationAction) {
Button("Close", action: close)
}
}// Objective-C: same semantics via UINavigationItem
self.navigationItem.pinnedTrailingGroup =
[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil
items:@[postButton]];
// Secondary actions go to the vertical bar's overflow menu
self.navigationItem.additionalOverflowItems =
[UIDeferredMenuElement elementWithProvider:^(void (^completion)(NSArray *)) {
completion(@[shareAction, archiveAction]);
}];竖排栏宽度固定,因此 Apple 直接建议使用纯符号按钮:文字标签和 segmented control 塞不进去——这类元素留在 navigation bar 里。想为某个界面单独关掉竖排布局也可以(SwiftUI 的 .toolbarVerticalBehavior(.disabled)、UIKit 的 preferredVerticalBarBehavior),但那应该是深思熟虑的决定,而不是默认选项。
Split View 和多窗口——第一次来到 iPhone#
Duo 是第一台支持 Split View 的 iPhone:两个应用 50/50 并排,而且所有应用都参与,没有任何 opt-in。你的应用随时可能只占半块屏——这是固定宽度不再成立的又一个理由。
第二个新特性是同一应用的多 scene,同样是 iPhone 上的第一次。如果你已经在 iPad 上支持多窗口(Info.plist 里的 UIApplicationSupportsMultipleScenes、用 scene 取代以 AppDelegate 为中心的生命周期),大部分工作已经完成。Info.plist 里没有任何 Duo 专属的 key。
但有一个 iPad 上不存在的行为:新窗口只能创建在内屏上。设备合着的时候,请求新 scene 会失败,这必须处理:
// Objective-C: the scene request can now fail
UISceneSessionActivationRequest *request =
[UISceneSessionActivationRequest requestWithRole:UIWindowSceneSessionRoleApplication];
[UIApplication.sharedApplication activateSceneSessionForRequest:request
errorHandler:^(NSError *error) {
// Device is closed — new windows are unavailable
[self showSingleWindowFallback];
}];在 SwiftUI 里,Apple 推荐使用标准的 UIWindowSceneActivation affordance:窗口创建不可用时它会自己隐藏,一半的手动处理就此省掉。
我的清单式计划#
为了不让这一切停留在纸面,等 Xcode 27.1 到手后,我会对自己的应用(MeteoHealth 先来)做这些事:
- 用 iOS 27.1 SDK 重新编译,在 Duo 模拟器里打开:Xcode 27.1 的 Device Hub 可以展开、合上、弯折虚拟设备。姿态相关的 bug 靠读代码是找不出来的。
- grep 写死的假设:
UIScreen.main、按方向和 idiom 的分支、对称 inset、硬编码宽度。 - 检查各个栏:我用的是 SwiftUI 加系统容器,竖排布局理论上是白送的——但按钮顺序和 overflow 优先级还是得亲眼过一遍。
- 跑一遍 Split View 的两种场景(应用在左、在右)以及两个半屏。
六场演讲看下来,诚实的结论是:如果应用本来就能自由缩放——用 size classes、按边尊重 safe area、活在系统容器里——那么 iPhone Duo 对它几乎什么都不改变。所有的痛苦都留给带着写死假设的应用。它们的时间在 10 月 23 日到期。
本系列下一篇会拆解铰链 API——onHingeChange 和 UIHingeInteraction——以及 Apple 为什么禁止用折叠角度驱动 layout。



