关于 iPhone Duo 的六场 Tech Talk 里没有一行 Objective-C。这在意料之中——Apple 用 Swift 和 SwiftUI 展示新东西。但行业的现实是另一回事:银行应用、即时通讯、一切从 2010 年代写起、熬过五轮改版的产品,核心里都还揣着 ObjC。做了八年 iOS,我见过的这类代码库足够多,多到我确信一件事:"先重写成 Swift 再适配"是一个永远不会发生的计划。要适配的,只能是手头现有的代码。
好消息是:Duo 需要的东西几乎全都住在 UIKit 里,而 UIKit 依然说 Objective-C。这篇文章讲清楚:哪些能直接调用,哪些有 UIKit 等价物,以及哪两个地方绕不开 Swift 桥接层。签名来自 2026 年 9 月的 Tech Talk;在稳定版 Xcode 27.1 发布前,请与 SDK 头文件核对拼写。
第 0 步:重新编译加审计#
第一个动作和语言无关:用 Xcode 27.1 对着 iOS 27.1 SDK 编译项目。不做这一步,就没有 edge-to-edge,没有竖排系统栏,没有任何新 API——应用只会在屏幕四周收获一圈留白。
接下来是 grep。以下每一个匹配项都是 Duo 上的潜在缺陷:
[UIScreen mainScreen] → ambiguous on a two-screen device
interfaceOrientation → the inner display ignores orientations
UI_USER_INTERFACE_IDIOM() → Duo is not a new idiom, idiom branches break
userInterfaceIdiom
safeAreaInsets.left * 2 → insets are asymmetric
hardcoded widths (390, 428, …) → width changes with a flick of the wrist老代码库里 [[UIScreen mainScreen] bounds] 动辄几十处——frame 计算、collection 配置、尺寸缓存。替代写法:
// Scale comes from the trait collection, not from the screen
CGFloat scale = self.traitCollection.displayScale;
// If you really need the screen (rare) — via the window scene
UIScreen *screen = self.view.window.windowScene.screen;
// Layout size — your own view's bounds inset by the safe area
CGRect content = UIEdgeInsetsInsetRect(self.view.bounds,
self.view.safeAreaInsets);用 size classes 取代方向判断——老办法,但这次动真格#
Trait collections 的 API 从 iOS 8 起就没变过——变的是无视它的代价。锁定竖屏的应用在 Duo 内屏上照转不误,supportedInterfaceOrientations 说了不算,所以一切"横屏时换个显示方式"的逻辑都必须搬到 size classes 上:
- (void)traitCollectionDidChange:(UITraitCollection *)previous {
[super traitCollectionDidChange:previous];
if (previous.horizontalSizeClass == self.traitCollection.horizontalSizeClass) {
return;
}
BOOL isWide = self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular;
// compact — Duo's outer screen (and every regular iPhone),
// regular — the inner screen (and iPad)
[self rebuildLayoutForWideMode:isWide];
}iOS 17 及以上,可以用注册式观察(registerForTraitChanges:)代替重写 traitCollectionDidChange:——它同样暴露给了 Objective-C。
审计还有单独的一项——Auto Layout 与手算 frame 的对比。约束到 safeAreaLayoutGuide 的布局不用改就能挺过 Duo;通过 self.view.frame.size.width 加偏移量手算的 frame 挺不过。迁移优先级应该给后者。
系统栏:清掉自制的,标注系统的#
Tech Talk 111462 里的规则对 legacy 代码打击最重:竖排布局只对系统容器生效——UINavigationController 和 UITabBarController。作为 subview 加进来的自定义 UIToolbar、用 UIView 拼的带按钮的自制"顶栏"——这些全都会保持水平,并和系统的竖排栏打架。
只要栏是系统的,新行为的标注完全可以在 Objective-C 里完成:
// Primary action — pin it in the vertical bar
self.navigationItem.pinnedTrailingGroup =
[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil
items:@[sendButton]];
// Custom back/close — leading item; stop supplementing the system button
self.navigationItem.leftItemsSupplementBackButton = NO;
// Secondary actions go to the overflow menu
self.navigationItem.additionalOverflowItems =
[UIDeferredMenuElement elementWithProvider:^(void (^completion)(NSArray *)) {
completion(@[self.shareAction, self.exportAction]);
}];
// Priorities: what leaves for overflow first when space runs out
filterButton.visibilityPriority = UIBarButtonItemVisibilityPriorityLow;
// A badge instead of a text counter
inboxButton.badge = [UIBarButtonItemBadge countBadgeWithInteger:7];竖排栏很窄,里面只放图标。"全部发送"这种文字按钮或 segmented control 塞不进去——这类元素留在 navigation bar。想让某个 controller 单独退出竖排布局——重写 preferredVerticalBarBehavior。
Scene:该还的债,到还的时候了#
很多 ObjC 应用至今活在以 AppDelegate 为中心的生命周期里,scene 迁移被推到"以后再说"。Duo 把"以后"定了日期:这是第一台单应用多窗口、人人参与 Split View 的 iPhone。迁移本身没什么特别(Info.plist 里 UIApplicationSceneManifest 加 UIApplicationSupportsMultipleScenes,用 UIWindowSceneDelegate 替换和 AppDelegate 的耦合)——没有任何 Duo 专属的 key。
新行为只有一个,但很阴险:窗口只在内屏上创建。设备合着时,scene 请求会失败:
UISceneSessionActivationRequest *request =
[UISceneSessionActivationRequest requestWithRole:UIWindowSceneSessionRoleApplication];
[UIApplication.sharedApplication activateSceneSessionForRequest:request
errorHandler:^(NSError *error) {
// Closed device: no new windows — open in the current window
[self openDocumentInCurrentWindow];
}];你 UI 里的"在新窗口打开"按钮要么用系统的 UIWindowScene activation affordance(窗口不可用时它自己隐藏),要么老老实实处理错误。
Objective-C 里的 reserved regions 和铰链#
折痕与摄像头的几何信息在 UIView 层面可以直接拿到:
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
if (folds.count > 0) {
CGRect foldFrame = folds.firstObject.frame;
// E.g.: keep the floating button out of this rectangle
}UIHingeInteraction 属于 UIInteraction 家族,因此同样兼容 ObjC:[view addInteraction:]。但在伸手去读铰链角度之前,值得先问一句到底需不需要它:按角度做 layout 被 Apple 明令禁止,而系统组件(sheet、alert、菜单、系统栏)不用你插手就会避开折痕。
Swift 到底哪里躲不掉#
诚实的边界是这样的。SwiftUI 独占的 API——onHingeChange、ArrangementView、带 CameraCaptureAccessory 的 .sceneAccessory——Objective-C 够不着,但前两者有完整的 UIKit 等价物(UIHingeInteraction、UIArrangementViewController),对大多数 legacy 应用来说这就够了。
Swift 桥接层在两种情况下必不可少。第一——需要显式管理摄像头模组的相机应用:AVCaptureDeviceDirectionCoordinator 隔离在 main actor 上,通过 AVCaptureDeviceDescriptor 传递设备——从 ObjC 调它形式上可行,但 Swift Concurrency 的 actor 模型在 ObjC 里根本不存在,更安全的做法是把相机协调封装成独立的 Swift 类型,对外露出 ObjC 兼容的门面。第二——外屏提词器这类双屏功能:CameraCaptureAccessory 是通过 SwiftUI 的 scene accessory API 声明的。
模式始终是同一个:一个小小的 @objc Swift 类,封装新 API,向 legacy 代码暴露 delegate 或 block。HealthKit、WidgetKit、App Intents 都是这么熬过来的——在这件事上 Duo 没有发明任何新东西。
一个季度的计划#
归结成清单,ObjC 应用的适配是这样的:
- 用 iOS 27.1 SDK 重新编译,在 Duo 模拟器(Device Hub)里跑一遍。
- grep 审计:
mainScreen、方向、idiom、对称 inset、硬编码宽度。 - 自制栏迁移到系统容器 + 标注 pinned/overflow。
- Scene:manifest、
UIWindowSceneDelegate、处理窗口创建失败。 - 相机和双屏的定点 Swift 桥接层——如果产品需要的话。
第 1–2 项以天计,第 3–4 项以周计、取决于欠下的债有多少,第 5 项不是人人都需要。到 10 月 23 日设备落入用户手中之前,前两项的时间任何团队都挤得出来。



