最后更新于 2026 年 9 月 14 日,数据核实自 Apple UIKit 文档、TN3187、Xcode 27 发布记录、App Store Connect 发布说明及 Apple Developer Forums。
Apple 官方文档已经明确:从 iOS 27 开始,使用最新 SDK 构建的 UIKit App 如果没有采用 UIScene 生命周期,将无法启动。Xcode 27 RC 已于 2026 年 9 月 9 日开放,App Store Connect 也已接受使用 iOS 27.0 RC SDK 构建的 App。结论很直接:只要你准备使用 iOS 27 SDK 构建,就应立即完成 iOS 27 UIScene 迁移;旧版 Xcode 只能作为短期回退,不能替代迁移。(Apple UIKit 生命周期迁移文档)
这篇文章适合仍由 AppDelegate 创建 UIWindow、尚未配置 UIApplicationSceneManifest 的存量 UIKit App。
如果你的项目使用 Storyboard、纯代码界面或混合架构,并且只有一台生产打包机,也可以按下面的分支建立隔离的 Xcode 27 验证环境。
01 先按最终构建结果判断:你是否必须迁移
不要只看测试设备是不是 iOS 27。真正决定启动行为的是最终构建使用的 SDK,以及安装包里的 Info.plist 和生命周期实现。
Apple 给出的判断条件有两个:
UIApplicationSceneManifest缺失,或者没有有效的场景配置;AppDelegate没有实现application(_:configurationForConnecting:options:)。
满足任意一项,就应把项目列为迁移对象。Apple 还说明,iOS 18.4 起,未迁移项目会出现警告;到 iOS 26,日志已经明确提示 UIScene 生命周期即将成为强制要求。(Apple TN3187 迁移说明)
你可以先在最终 Archive 解包后的 Info.plist 中检查:
/usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest" \
Payload/YourApp.app/Info.plist
再检查 AppDelegate 是否存在类似代码:
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}
触发强制要求的根本原因是什么?
因为 UIKit 已经把进程生命周期和界面生命周期拆开管理。App 进程由 UIApplicationDelegate 处理,而每个可见界面由 UISceneDelegate 或 UIWindowSceneDelegate 处理。iOS 27 对使用最新 SDK 构建的 App 执行这一要求,不是因为你启用了多窗口,也不是因为部署目标必须设置为 iOS 27。
因此,不要把“支持 UIScene”和“开放多窗口”混为一谈。你可以只配置一个主窗口场景,先完成生命周期迁移,再决定是否支持多窗口。
决策条件:现在该迁移还是先回退
- 若最终构建使用 iOS 27 SDK:立即迁移,并在安装后验证真实启动。
- 若当前版本必须马上发布,迁移尚未完成:暂时使用已验证的旧 SDK 构建,但把它标记为回退产物。
- 若项目只在旧 Xcode 编译,尚未进入发布周期:可以短期维持旧工具链,但不要据此判断 iOS 27 兼容。
- 若生产打包机只有一台:不要直接切换默认 Xcode。先复制脱敏项目到隔离 Mac,完成迁移和 Archive 验收。
Apple 工程师在开发者论坛中进一步说明,要求是在使用 iOS 27.0 SDK 构建时执行;使用旧 SDK 构建的 App 暂时不会执行该要求。但 Apple 尚未公布未来强制使用新 SDK 提交 App 的具体日期,因此旧工具链只能承担回退职责。(Apple Developer Forums 关于 SDK 触发边界的说明)
02 第一类项目:Storyboard 入口要避免重复创建窗口
如果你的 UIKit 项目依赖 Main Storyboard,迁移重点不是盲目新建一个 SceneDelegate.swift,而是确认场景配置和 Storyboard 入口只有一条有效链路。
在 Target 的 Info 配置中,检查 Application Scene Manifest 下是否存在主场景配置。通常需要确认以下信息:
- 场景类型是
UIWindowScene; - 场景代理类名指向你的
SceneDelegate; Storyboard Name与实际文件名一致;- Storyboard 内存在明确的 Initial View Controller;
UIApplicationSupportsMultipleScenes是否符合项目实际需求。
使用 Storyboard 时,UIKit 可以根据场景配置自动创建窗口,并把初始控制器装入窗口。此时如果你又在 SceneDelegate 中重新创建一套窗口,或者继续从 AppDelegate.window 注入界面,就容易出现空白界面、重复初始化或导航栈丢失。(Apple 场景配置文档)
建议把职责拆成两层:
AppDelegate:进程级初始化,例如日志、依赖容器、推送注册、数据库准备;SceneDelegate:当前界面的连接、前后台变化、窗口级状态和界面恢复。
迁移后不要只看编译是否通过。至少执行下面三组验收:
- 冷启动:杀掉 App 后,从桌面图标启动;
- 前后台切换:进入后台,再从任务切换器恢复;
- 状态恢复:停留在非首页界面后结束进程,再检查恢复结果。
如果冷启动成功但前后台恢复后出现重复登录、导航栈回到首页,说明仍有全局生命周期代码没有按场景拆开。
03 第二类项目:纯代码 UIKit 必须重新接通 UIWindow
纯代码项目最容易出现“编译通过、启动黑屏”。原因通常是以前在 AppDelegate 中完成了窗口创建,而启用场景后,系统把界面连接入口交给了 SceneDelegate。
一个最小的纯代码窗口链路应当明确包含三件事:
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else {
return
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = makeRootViewController()
self.window = window
window.makeKeyAndVisible()
}
}
这里的关键不是代码长短,而是关系必须完整:
- 从连接参数取得
UIWindowScene; - 用这个场景创建
UIWindow; - 设置根控制器;
- 保存窗口引用;
- 让窗口可见。
场景对象代表一个独立的用户界面实例,窗口和控制器都应绑定到对应的 UIWindowScene。继续依赖 AppDelegate.window 或全局 keyWindow,在多场景、外接显示和窗口重建时都可能拿到错误对象。(Apple UIScene API 文档)
旧项目只有 AppDelegate 时,迁移路径应怎样安排?
先不要删除 AppDelegate 中的所有代码。按下面顺序移动:
- 将窗口创建和根控制器设置移到
scene(_:willConnectTo:options:); - 将界面激活逻辑从
applicationDidBecomeActive移到sceneDidBecomeActive; - 将界面即将离开前台的处理移到
sceneWillResignActive; - 将界面状态保存逻辑移到
sceneDidEnterBackground; - 保留进程级初始化在
application(_:didFinishLaunchingWithOptions:); - 最后搜索并清理
UIApplication.shared.keyWindow、AppDelegate.window等旧引用。
迁移说明列出了常见生命周期方法的对应关系,但不会替你判断业务代码属于进程还是界面。支付回调、网络层、数据库连接不一定要搬到 SceneDelegate;导航、当前用户界面、窗口引用通常需要搬。
04 第三类项目:深度链接、推送和登录回调要按启动路径分流
迁移后最隐蔽的问题,往往不是窗口,而是回调入口。以前所有事件都集中在 AppDelegate,启用场景后,同一个事件可能发生在冷启动、后台唤醒或已有场景激活三种状态。
场景连接时,UIScene.ConnectionOptions 可以携带:
urlContexts:自定义 URL Scheme 或其他 URL;userActivities:Universal Link、Handoff 等用户活动;notificationResponse:用户点击通知后的响应;shortcutItem:主屏幕快捷操作。
这些连接参数有明确分类,不能继续把所有跳转都当作 didFinishLaunchingWithOptions 的启动参数处理。(Apple ConnectionOptions 文档)
推荐按路径建立入口:
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
// 先创建窗口,再处理冷启动携带的 URL、通知和 user activity
}
func scene(
_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>
) {
// 处理 App 已连接或已激活时收到的 URL
}
第三方登录、分析 SDK 和推送 SDK 要逐个检查。重点搜索以下旧入口:
application(_:open:options:)application(_:continue:restorationHandler:)application(_:didReceiveRemoteNotification:)application(_:didReceive:)application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
并不是每个方法都要删除。你需要判断它处理的是设备注册、进程事件,还是界面跳转。如果是“打开某个页面”,应先把事件转换成应用内部路由,再交给当前场景;如果当前没有可用场景,则等待场景连接后处理。
深度链接、推送和登录回调分别应该从哪里接收?
冷启动携带的数据,先从 connectionOptions 读取;App 已经有场景时,URL 使用 scene(_:openURLContexts:),通知则从场景连接参数或对应的通知响应路径读取。Universal Link 通常以 NSUserActivity 形式进入场景,不要只测试桌面图标启动。
测试时使用脱敏的 URL、通知载荷和账号。不要把真实用户标识、推送 Token、Bundle ID、Team ID 或主机地址写进日志和文章截图。
05 多窗口、iPad 与 Mac Catalyst 不要被基础迁移带偏
采用 UIScene 不等于必须立即重写为文档型 App。对于只需要一个主窗口的独立 App,可以先配置一个主场景,确保生命周期和回调正确,再评估多窗口。
但以下项目需要额外检查:
- iPad 的 Split View、Slide Over 和 Stage Manager;
- 文档型 App 的场景与文档绑定;
- Mac Catalyst 的窗口创建和关闭;
- 外接显示器的场景角色;
- 共享可变状态是否错误地假设只有一个界面;
- 场景断开后,定时器、观察者和临时资源是否释放。
对于外接显示场景,过去根据外接显示角色手动提供内容的项目,需要重新对照最新文档检查是否应使用场景附件,而不是直接扩大迁移范围进行架构重写。(Apple UIKit 生命周期迁移文档)
⚠️ 注意:不要用“支持多窗口”作为迁移完成标准。本文的最低完成标准是:使用 iOS 27 SDK 构建后,App 能真实安装、冷启动、恢复、接收链接并完成 Archive。
06 发布前用双轨环境保存回退能力
Xcode 27 RC 已在 2026 年 9 月 9 日发布,Apple 的发布记录同时列出了 iOS 27.0 RC。App Store Connect 发布说明也确认,可以上传使用 Xcode 27 RC 和 iOS 27.0 RC SDK 构建的 App。(Apple Xcode 发布记录)
但不要直接在生产打包机上覆盖默认 Xcode。更稳妥的流程是:
第一步:复制脱敏项目
删除真实账号、签名文件、推送载荷、私有接口地址和生产配置。保留与启动相关的 Target、Scheme、Info.plist、Storyboard 和第三方 SDK。
第二步:记录旧工具链基线
使用当前生产工具链执行:
BuildTestArchive- 安装到测试设备或模拟器
- 冷启动和深度链接验证
保存构建日志、Archive 路径和启动结果。旧产物的作用是回退,不是证明已经支持 iOS 27。
第三步:在隔离环境切换 Xcode 27
不要覆盖旧版本。使用明确的 xcode-select 或 CI 环境变量选择工具链,并记录当前路径:
xcode-select -p
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version
Xcode 27 只支持 Apple Silicon Mac 安装和运行,Apple 的 Xcode 27 发布说明也提醒了这一兼容边界。(Apple Xcode 27 Release Notes)
第四步:完成迁移并逐场景验证
按项目类型验证:
- Storyboard:检查场景配置和初始控制器;
- 纯代码:检查
UIWindowScene、根控制器和窗口引用; - 混合架构:检查 Storyboard 自动窗口与代码窗口是否重复;
- 登录和推送:覆盖冷启动、后台唤醒、已激活三种状态;
- 状态恢复:检查被系统终止后是否回到正确界面。
第五步:单独执行 Archive 和安装后启动
不要把“Build 成功”当作“发布通过”。必须从 .xcarchive 导出安装包,在真实设备上安装,然后从桌面图标启动。对启动失败,保存设备日志、崩溃信息和最终 Info.plist。
第六步:定义生产切换与回退条件
满足以下条件后,才考虑把 Xcode 27 设为默认工具链:
- 迁移版冷启动通过;
- 深度链接和推送路径通过;
- 前后台切换和状态恢复通过;
- Archive、导出、安装和真实启动通过;
- 无人值守构建可以在远程会话断开后继续;
- 主机重启后,工具链路径、证书和缓存仍然可用。
如果任意一项失败,就回退到已验证的旧工具链,同时保留迁移分支和失败证据。你可以把脱敏项目放在 CALMVPS 的 Mac 远程租赁环境 中,隔离验证 Xcode 27,而不是冒险改动唯一的生产 Mac。需要长期保留多套工具链时,再参考 CALMVPS 的方案与计费页面 评估周期。
07 迁移完成验收清单
提交 App Store Connect 前,逐项勾选:
- [ ] 最终 Archive 使用的 SDK 已确认;
- [ ]
UIApplicationSceneManifest存在且配置有效; - [ ]
application(_:configurationForConnecting:options:)行为明确; - [ ] Storyboard 项目没有重复创建窗口;
- [ ] 纯代码项目在
scene(_:willConnectTo:options:)创建并显示窗口; - [ ] 代码中不再依赖全局
keyWindow获取当前界面; - [ ]
AppDelegate与SceneDelegate的职责已经拆开; - [ ] 冷启动可以进入正确首页;
- [ ] 后台唤醒不会重复初始化导航栈;
- [ ] 自定义 URL、Universal Link 和推送点击均能路由;
- [ ] iPad 多任务场景没有错误窗口引用;
- [ ] Mac Catalyst 或外接显示逻辑已按项目实际情况复核;
- [ ] 旧工具链仍能生成可安装的回退产物;
- [ ] Xcode 27 环境可以在远程断线、主机重启后重复构建;
- [ ] 生产打包机尚未在迁移验收完成前切换默认 Xcode。
如果你现在的方案是把 Xcode 27 直接装到唯一的生产 Mac 上,主要风险有三个:新工具链可能影响现有签名和缓存;迁移版启动失败会堵住正常发布;远程无人值守构建缺少可回退环境。对需要并行维护旧版本和 iOS 27 版本的独立开发者来说,先在隔离的远程 Mac 上复制项目、完成真实启动与 Archive 验收,通常比立即更换生产工具链更稳妥。通过验收后,再把变更安排到生产环境;如果只是临时测试、双版本构建或发布前验证,租用 CALMVPS 的 Mac 环境会比为一次迁移专门购买并长期维护第二台 Mac 更灵活。