UIScene 生命周期迁移与 Xcode 27 启动适配#

Jobs出品,必属精品

📚 目录(点击展开 / 收起)

🔥 前言#

资料核对日期:2026-10-15。 本文面向 Objective-C 新旧工程、Swift / UIKit 工程,说明为什么必须迁移、启动流程具体改变了什么、代码该放在哪里,以及如何验收。

从 iOS / iPadOS 27 起,使用最新 SDK 构建的 UIKit App 必须采用 scene 生命周期,否则无法启动。“用 Xcode 27 编译就一定在所有系统上崩溃”不够准确:需要分清构建所链接的 SDK、运行系统,以及最低部署版本。AppDelegate 仍然存在;迁移的核心,是把进程初始化与每个窗口的 UI 初始化分开。Apple 迁移文档

本文用可编辑的 Mermaid 时序图重点对照以前和今天的启动流程。图中的回调名缩短显示,完整 OC / Swift 名称见回调对照。普通前台启动、后台唤醒、恢复已有会话是不同路径,不把所有回调画成每次必定发生。

快速入口:强制门槛 · 旧启动流程 · 新启动流程 · 进程存活时的新建与重连 · 配置 · 入口事件 · 验收 · 工程落地记录

一、先说准确:强制的是 scene 生命周期 🔼 🔽#

1.1、三个版本维度不能混用 🔼 🔽#

维度负责什么与本问题的关系
Xcode 版本IDE、编译器、调试与打包工具Xcode 27 通常带来 iOS 27 SDK;IDE 版本名称不是完整触发条件
构建 SDK编译链接时采用的系统接口及链接行为官方要求针对“使用最新 SDK 构建”的 App
设备运行系统实际执行 App 的系统iOS / iPadOS 27 开始执行新的启动要求
最低部署版本App 允许安装到的最低系统降低该值不能规避新 SDK 在新系统上的启动要求

Apple 在 WWDC25:Make your UIKit app more flexible 预告下一大版本的强制要求;WWDC26:Modernize your UIKit app 的 2:10 章节确认,使用最新 SDK 时没有 scene 生命周期的 App 将无法启动。最终条件以当前迁移文档为准,不从“旧包在新系统上还能打开”推导“升级 SDK 后也安全”。

工程判断:不要为了绕过要求长期停留在旧 SDK,也不要把部署目标强行抬到 27 当成修复。完成 scene 迁移后,旧系统支持范围仍由业务、依赖和 API 可用性决定。

1.2、单窗口也必须有 scene,多窗口仍是产品选择 🔼 🔽#

  • UIApplicationSupportsMultipleScenes = false:采用 scene 生命周期,同时只显示一个 scene;这可以满足生命周期要求。
  • UIApplicationSupportsMultipleScenes = true:允许多个 scene 同时存在;每个 scene 的窗口、导航栈、页面状态都要独立。
  • 只有一个 SceneDelegate 文件,不代表运行时只有一个代理实例。系统为不同连接创建各自的场景及代理。

三份 Jobs 基础工程原本已经启用多 scene,并有独立 SceneDelegate Demo。因此工程修复应保留多窗口能力,修正遗漏的配置、事件与窗口归属,不通过关闭多窗口掩盖全局窗口问题。TN3187

1.3、旧警告与实际失败要区分 🔼 🔽#

旧系统会记录“尚未采用 scene、将来会触发断言”的迁移提示;新要求生效后,问题不再只是弃用警告。出现启动断言时,先查看运行日志、最终 App 的 Info.plist 和实际回调,不能只检查工程目录里是否存在 SceneDelegate 文件。TN3187 的版本说明

二、以前与今天:真正改变的是启动流程 🔼 🔽#

2.1、以前:AppDelegate 同时初始化进程与唯一主窗口 🔼 🔽#

下图描述未采用 scene 的纯代码 UIKit App,从未运行到前台可交互的典型冷启动。它用于理解旧架构,不能再作为新 SDK 的目标实现。若旧工程采用主 storyboard,窗口和初始控制器的创建由 UIKit 承接,不能再机械套用图中的手动创建步骤。

sequenceDiagram
    autonumber
    actor U as 用户
    participant OS as 系统
    participant E as main或Swift入口
    participant K as UIKit
    participant A as AppDelegate
    participant W as 唯一主UIWindow
    participant R as 根控制器
    U->>OS: 点击App图标
    OS->>E: 启动App进程
    E->>K: UIApplicationMain
    K->>K: 创建UIApplication及AppDelegate
    K->>A: willFinishLaunching(若实现)
    opt 启用了旧式控制器状态恢复
        K->>A: 状态恢复相关回调
    end
    K->>A: didFinishLaunching(launchOptions)
    A->>A: 初始化进程服务
    A->>W: 创建并持有窗口
    A->>R: 创建根控制器与导航栈
    A->>W: 安装rootViewController
    A->>W: makeKeyAndVisible
    A-->>K: 返回启动结果
    K->>A: applicationDidBecomeActive
    Note over A,R: 窗口构建、全局服务、界面激活集中在AppDelegate

OC 的 main.m 通常显式调用 UIApplicationMain;UIKit Swift 工程的 @main AppDelegate 由入口机制衔接。@main 本身不意味着已经使用 scene。旧架构常见的问题,是任何模块都去读取 AppDelegate.window,并假定整个进程只有这一套 UI。

2.2、今天:先启动进程,再连接一个窗口场景 🔼 🔽#

下图仍是普通前台冷启动、纯代码创建窗口。图中“新会话”与“恢复已有会话”是替代分支:冷启动也可能恢复上次保留的 session,不能把冷启动等同于永远新建 session。

sequenceDiagram
    autonumber
    actor U as 用户
    participant OS as 系统
    participant E as main或Swift入口
    participant K as UIKit
    participant A as AppDelegate
    participant S as 本次连接的SceneDelegate
    participant W as 绑定本scene的UIWindow
    participant R as 本scene的根控制器
    U->>OS: 点击图标或打开外部入口
    OS->>E: 启动App进程
    E->>K: UIApplicationMain
    K->>K: 创建UIApplication及AppDelegate
    K->>A: willFinishLaunching(若实现)
    opt 启用了兼容的控制器状态恢复
        K->>A: 状态恢复相关回调
    end
    K->>A: didFinishLaunching
    A->>A: 初始化进程共享服务
    A-->>K: 返回,尚不创建业务主窗口
    alt 需要创建新的session
        K->>K: 读取manifest中的初始配置
        opt 实现了动态配置回调
            K->>A: configurationForConnecting(session, options)
            A-->>K: 返回本session的UISceneConfiguration
        end
    else 重新连接已存在的session
        K->>K: 使用该session保存的配置
        Note over K,A: 不依赖配置回调再次运行
    end
    K->>K: 按配置准备UIWindowScene及代理实例
    K->>S: willConnect(scene, session, connectionOptions)
    S->>W: 创建窗口并绑定传入的UIWindowScene
    S->>R: 创建该scene独立的根控制器与导航栈
    S->>W: 安装rootViewController并由代理持有窗口
    S->>S: 解析connectionOptions并按需排队路由
    S->>W: makeKeyAndVisible
    S-->>K: 返回
    Note over K,R: 有恢复状态时在willConnect后交付restoreInteractionStateWith<br/>此处不规定它与前台及active回调的相对顺序
    K->>S: sceneWillEnterForeground
    K->>S: sceneDidBecomeActive
    S->>R: UI就绪后执行待处理入口
    Note over A,R: AppDelegate负责进程;SceneDelegate负责本次UI连接

图中把 UIWindowScene 与代理实例的准备作为系统步骤,不规定二者内部构造的先后。业务窗口部分展示职责与依赖,不要求两种语言逐行采用完全相同的构造顺序:Swift 宿主实际先构造 root,再交给 UIWindow.jobsMake(scene:root:) 工厂;OC 链先创建窗口,再设置 root。两者都必须在展示前完成 scene 绑定、root 安装与窗口持有。

makeKeyAndVisible 只是设置窗口为可见并请求成为 key window,不等于 scene 已经 active,也不等于开屏、登录或首屏数据已经就绪。路由应由工程的 UI 就绪条件驱动,不能任意延时后猜测。

系统可以仅为后台事件启动进程,不必立刻创建可见业务窗口;这种路径不会照图走到前台 active。相关依据:Apple 启动顺序、响应启动、scene 连接回调。

2.3、进程还活着:新建窗口、重连窗口、重新激活是三条路 🔼 🔽#

sequenceDiagram
    autonumber
    actor U as 用户或外部事件
    participant K as UIKit
    participant A as 已存在的AppDelegate
    participant S as 目标scene的代理
    participant W as 目标scene的窗口与根控制器
    Note over K,A: App进程已经运行;不再执行main及launch回调
    U->>K: 请求打开或激活某个窗口
    alt 新建session
        K->>K: 从manifest选取初始配置
        opt 实现动态配置回调
            K->>A: configurationForConnecting(session, options)
            A-->>K: 返回scene配置
        end
        K->>K: 创建新的UIWindowScene及代理
        K->>S: willConnect(scene, newSession, options)
        S->>W: 新建独立窗口、根控制器并绑定scene
        S->>W: makeKeyAndVisible
        K->>S: 目标scene的前台与激活回调
    else 已有session已断开,需要重连
        K->>K: 使用session保存的配置重建scene及代理
        K->>S: willConnect(scene, existingSession, options)
        S->>W: 重建本session的窗口与根控制器
        Note over K,W: willConnect后按需交付恢复活动<br/>恢复逻辑不依赖它与前台回调的固定相对顺序
        K->>S: 目标scene的前台与激活回调
    else 目标scene仍然连接
        Note over K,W: 复用现有窗口,不重建根控制器
        opt 从后台或非活跃状态回到前台
            K->>S: 对应的前台与激活回调
        end
        opt 本次带有外部入口
            K->>S: openURLContexts或continueUserActivity等
            S->>W: 在该scene路由
        end
    end

图中的前台、激活与外部事件只表示各自适用的步骤,不承诺所有外部事件都固定在 active 之后到达。业务层必须兼容“事件先到、UI 尚未就绪”。配置回调用于配置创建,不用来承接每次窗口激活都必须执行的业务;每次新建或恢复 UI 的连接准备放在 willConnect。场景配置 API、连接 API

2.4、哪些只执行一次,哪些会重复 🔼 🔽#

启动步骤执行边界应放内容不应放内容
main / UIKit Swift 入口每次进程启动交给标准入口机制数据库迁移、网络请求等重工作
AppDelegate willFinish / didFinish每次进程启动共享服务、通知代理、日志与全局配置创建所有窗口、重建各窗口的页面
configurationForConnecting新 scene 配置选择;不是每次激活根据 role、activity 选择配置窗口展示、消费并丢弃冷启动路由
SceneDelegate willConnect每次创建或恢复 UI 连接scene 绑定窗口、根控制器、收集连接入口重复注册全进程服务
restoreInteractionStateWith系统提供恢复状态时恢复本 session 的页面交互状态覆盖本次用户主动打开的目标
sceneWillEnterForeground对应 scene 进入前台刷新本窗口需要的数据断言整个进程刚启动
sceneDidBecomeActive对应 scene 获得交互状态恢复动画、检查本窗口待处理路由每次重建 window、root 或开屏

“启动一次”默认指一次进程启动,不等于 App 首次安装后的第一次运行。安装初始化需要持久标记;多窗口共享服务需要幂等初始化;UI 初始化则以本次 scene 连接为单位。启动职责分配

三、职责拆分与完整回调对照 🔼 🔽#

3.1、进程、会话、场景、窗口各管什么 🔼 🔽#

对象作用保存什么
UIApplication / AppDelegateApp 进程与共享能力通知注册、共享服务、scene 配置策略
UISceneSession可持续的 UI 会话身份persistentIdentifier、配置、恢复活动
UIWindowScene当前连接中的 UI 环境scene 状态、显示环境、本 scene 的窗口集合
SceneDelegate单个 scene 的回调入口本 scene 的 window、路由与恢复上下文
UIWindow承载根控制器与视图绑定的 windowScene、窗口级主题与层级

一个 session 断开后仍可能保留,后续连接时重新建立内存对象。不要拿 SceneDelegate 指针当永久会话身份,也不要把 sceneDidDisconnect 当成用户永久删除会话。UISceneDelegate、UIWindowSceneDelegate

3.2、OC 与 Swift 回调一一对应 🔼 🔽#

用途Objective-C selectorSwift 方法
进程启动完成application:didFinishLaunchingWithOptions:application(_:didFinishLaunchingWithOptions:)
选择 scene 配置application:configurationForConnectingSceneSession:options:application(_:configurationForConnecting:options:)
连接 scenescene:willConnectToSession:options:scene(_:willConnectTo:options:)
前台sceneWillEnterForeground:sceneWillEnterForeground(_:)
活跃sceneDidBecomeActive:sceneDidBecomeActive(_:)
失去活跃sceneWillResignActive:sceneWillResignActive(_:)
后台sceneDidEnterBackground:sceneDidEnterBackground(_:)
断开sceneDidDisconnect:sceneDidDisconnect(_:)
URLscene:openURLContexts:scene(_:openURLContexts:)
用户活动scene:continueUserActivity:scene(_:continue:)
快捷操作windowScene:performActionForShortcutItem:completionHandler:windowScene(_:performActionFor:completionHandler:)
提供恢复活动stateRestorationActivityForScene:stateRestorationActivity(for:)
恢复交互状态scene:restoreInteractionStateWithUserActivity:scene(_:restoreInteractionStateWith:)

采用 scene 后,原 AppDelegate 的 applicationDidBecomeActive、applicationWillResignActive、applicationDidEnterBackground、applicationWillEnterForeground 不再承担 UI 生命周期;保留旧实现不意味着 UIKit 还会继续调用。将对应逻辑迁移到 scene,并检查共享服务是否需要进程聚合状态。Apple 生命周期迁移说明

四、配置必须完整,最终包必须能解析到类 🔼 🔽#

4.1、纯代码窗口的 manifest 基线 🔼 🔽#

以下是采用 SceneDelegate 类名的 OC 示例,放入 App target 实际使用的 Info.plist 字典中。该示例保留多窗口;产品只需一个窗口时可以把 true 改为 false,无需退回 AppDelegate 管理窗口。

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneClassName</key>
                <string>UIWindowScene</string>
                <key>UISceneDelegateClassName</key>
                <string>SceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

Swift 的默认运行时类名带模块前缀,一般将上面的代理类值替换为 $(PRODUCT_MODULE_NAME).SceneDelegate。若类通过 @objc(...) 显式指定了运行时名称,以真实名称为准。不能把 Swift 带模块写法直接照搬给普通 OC 类,也不能凭文件名猜测运行时类。scene 配置字段

4.2、静态配置与动态配置二选一,也可以显式互相校验 🔼 🔽#

  • 配置固定:manifest 可以完整描述 scene,不必为了形式增加动态配置回调。
  • 需要按 session role、用户活动或内容类型选场景:实现 AppDelegate 的配置回调。使用类型设置 sceneClass、delegateClass 可以避免只靠字符串查类;配置名与 manifest 保持一致。
  • UISceneConfigurations 可以由完整的动态配置回调补足。只有 manifest 配置为空 / 只写了多窗口开关,且动态回调也没有正确提供匹配 role 的 scene / delegate 配置时,才属于配置缺失;不能把“没有静态数组”一概判错。
  • 旧安装保留的 session 可能带着旧配置。测试需要同时覆盖干净安装与保留数据升级;不能只通过卸载重装得到一次成功就结束。

动态配置回调返回的是“如何创建 scene”的描述;窗口仍由 UIKit 交付 scene 后,在代理连接方法里建立。动态配置 API

4.3、storyboard 与纯代码创建要明确选路 🔼 🔽#

UI 方案必要设置窗口创建方
纯代码正确 scene / delegate 类;不配置业务 UISceneStoryboardFileSceneDelegate 创建并绑定 UIWindowScene
storyboardscene 配置中指定 storyboard 名,存在 initial view controllerUIKit 自动创建 scene 的窗口与初始根控制器

启动屏幕 UILaunchStoryboardName / UILaunchScreen 与业务主 storyboard 是不同配置;删除旧业务 UIMainStoryboardFile 时,不要误删启动屏幕。LaunchScreen 展示不是业务窗口已经创建的证据。配置初始 UI

五、OC 新旧工程与 Swift 工程怎么迁移 🔼 🔽#

5.1、先查已有实现,避免重复迁移 🔼 🔽#

1、确认 App target 的入口、INFOPLIST_FILE、manifest、SceneDelegate 及源码 target membership。磁盘有文件但没有加入 target,运行时同样找不到代理。

2、检查 didFinishLaunching 是否仍创建 UIWindow、设置全局 root、展示开屏;检查 scene 已连接后是否又建立第二个窗口。

3、逐项盘点 URL、Universal Links、快捷操作、通知响应、主题、调试悬浮窗、媒体恢复和页面状态,记录旧入口与新入口。

4、检查窗口检索封装及其调用方,重点看 AppDelegate.window、connectedScenes.first、UIApplication.windows、全局 key window 与 UIScreen.main.bounds。

5、区分主工程、Jobs 自建能力与第三方。OC 新工程的 Jobs 自建能力按本地 Pod 形态修改,OC 老工程按原有主工程集成位置同步;不把新工程 Pod 目录直接搬进老工程,也不修改供应商或生成源码。

5.2、窗口创建的必要不变量 🔼 🔽#

无论 OC 还是 Swift,连接方法都必须做到:确认传入的是 UIWindowScene → 用这个 scene 建窗口 → 为该窗口建立独立根控制器 / 导航栈 → SceneDelegate 持有窗口 → 展示窗口。不能先拿“任意前台 scene”建窗口再回填,也不能把旧窗口对象跨 scene 复用。

Jobs 工程调用方沿用现有 JobsMake / JobsOCDSL、JobsSwiftDSL 创建与链式配置入口;底层封装负责承接 Apple 的 scene 绑定 API。迁移时核对封装真实实现,不能只把调用名称换成 DSL 就宣称绑定正确。

5.3、SwiftUI App 单独判断 🔼 🔽#

SwiftUI 的 @main struct ...: App,在 body 中通过 WindowGroup 等 Scene 声明 UI,已经采用系统管理的 scene 生命周期。它与 @main class AppDelegate: UIApplicationDelegate 是两种入口形态;只看 @main 无法判断。

不要为 SwiftUI App 再手动创建一套业务主 UIWindow 与 SwiftUI 竞争。确需通知注册等 AppDelegate 能力时,通过 @UIApplicationDelegateAdaptor 桥接;场景变化优先使用 scenePhase,链接在相应视图 / scene 的入口处理。SwiftUI 的内部代理创建过程由框架管理,不将 UIKit 图中所有内部步骤强套成 SwiftUI 的公开顺序。App、UIApplicationDelegateAdaptor

六、冷启动与热启动:必须同时迁移入口事件 🔼 🔽#

6.1、冷连接使用 connectionOptions,热事件使用专属回调 🔼 🔽#

入口创建或恢复 scene 的连接入口scene 已连接时的入口必须保留的上下文
URL Scheme / 文件 URLconnectionOptions.urlContextsscene:openURLContexts: / scene(_:openURLContexts:)URL、来源与打开选项、目标 scene
Universal Links / HandoffconnectionOptions.userActivitiesscene:continueUserActivity: / scene(_:continue:)activity 类型、webpageURL 或业务数据、目标 scene
主屏快捷操作connectionOptions.shortcutItemwindowScene:performActionForShortcutItem:completionHandler:item 类型、参数、完成结果
点击通知connectionOptions.notificationResponse(因通知建立 scene 时)通知中心代理的 didReceive responserequest 标识、action 标识、载荷、路由目标
session 状态恢复session.stateRestorationActivity;恢复回调状态保存 / 恢复 API本 session 的内容身份与页面状态

connectionOptions 表达“这次为什么连接这个 scene”,不只对应“进程冷启动”。进程已运行但新建窗口时也可能带同样的连接数据。反过来,scene 已连接时的热 URL 不会要求重新执行 willConnect。ConnectionOptions、URL 回调、activity 回调、快捷操作回调

采用 scene 后,AppDelegate 的 willFinish / didFinish 仍用于进程初始化,但不能再依赖其 launchOptions 承接 scene 的启动原因。冷启动只改热事件回调,常见结果就是“App 已打开,用户指定的页面没有打开”。Apple 对 launch options 的说明

6.2、路由必须等 UI 就绪,同时保证只消费一次 🔼 🔽#

工程建议:连接与热事件先归一化为业务意图,附带目标 session;root / 开屏 / 登录等准备完成后再消费。UI 线程负责导航,异步准备结束前保留意图;scene 断开时释放连接资源,必要的可恢复意图按会话持久化。

必须明确以下规则,不能依赖碰巧成功的回调顺序:

  • 本次外部打开与旧页面恢复同时存在时,主动入口优先;不允许恢复逻辑把刚打开的详情覆盖回旧页面。
  • SDK 回调、通知中心回调与 scene 连接数据可能进入同一业务链,按稳定业务标识去重,避免重复 push / present。
  • urlContexts / userActivities 是集合;不要把无序 first 当成业务优先级。支持多个就逐个处理;只支持一个时建立明确筛选规则。
  • 登录和开屏各自异步时,使用实际完成信号与可路由 root;sceneDidBecomeActive 不能替代所有就绪条件。
  • 快捷操作 completion handler 无论成功或失败都恰好调用一次。无法识别的动作返回失败;不要让外部系统一直等候。

6.3、通知注册与 UI 路由不能整段搬走 🔼 🔽#

UserNotifications 通知中心代理须在进程启动结束前设置,通知授权与远程推送注册仍是进程级工作;设备 token、静默推送与后台处理也不能机械搬成某个窗口私有逻辑。点击通知导致 UI 连接时,再读取 scene 的 notification response,将目标页面送到对应窗口。通知中心代理、connectionOptions.notificationResponse

官方通知中心代理与 scene 连接入口的职责不同。共享通知路由器应有单一消费策略、去重标识、明确的目标选择,以及所有 completion handler 的收口;后台处理不要假定存在可见窗口。

七、前后台与状态恢复按场景治理 🔼 🔽#

7.1、一个窗口后台,不等于整个 App 后台 🔼 🔽#

页面动画、摄像头预览、交互计时等跟随所属 scene;共享连接、后台同步和进程统计根据全进程状态处理。窗口 A 进入后台时,窗口 B 可能仍在前台,不能在 A 的 sceneDidEnterBackground 无条件关闭全进程服务。

需要“整个进程是否仍有前台界面”时,使用 UIApplication 对应通知与聚合状态,或维护按 session 标识记录的场景状态集合。不要只转发某一个 SceneDelegate 回调,伪装成整个进程状态。生命周期文档、当前迁移文档的重要说明

7.2、会话恢复与业务数据持久化是两件事 🔼 🔽#

SceneDelegate 通过 stateRestorationActivityForScene: / stateRestorationActivity(for:) 提供 NSUserActivity,保存内容标识、选中标签、导航位置等 UI 状态;恢复时按对应 session 读取。活动中不要塞窗口、控制器、巨大缓存或敏感凭据。

工程建议:数据本体由业务存储保证;恢复活动只保存重新定位所需的轻量信息。内容已删除、权限改变、账户退出时,安全回退到可用页面。不要等 applicationWillTerminate 才保存,后台挂起后的进程终止不保证先给这个机会。

测试状态恢复要采用官方样例建议的后台后停止调试等路径;调试时从 App 切换器强退可能清除恢复状态。断开重连、用户主动关闭 scene、系统回收、全进程重启分别测试,不能用一次普通前后台切换代替。Apple 状态恢复样例

八、窗口归属、悬浮窗与布局要一并审计 🔼 🔽#

8.1、从当前 UI 找场景,不从全局集合猜场景 🔼 🔽#

手中已有的上下文应该怎样取得窗口 / scene
当前 view使用 view.window,再取 windowScene
当前 view controller在视图已进入窗口后使用其 view 的 window;没有窗口时延后或传入上下文
scene / window 回调参数使用系统本次交付的 scene / window
业务异步任务显式携带发起方 session / scene 身份,完成时验证该连接仍有效
跨窗口管理界面明确选中目标 session,再执行激活、关闭或更新

connectedScenes.first 的顺序没有业务含义;多个 scene 可同时在前台,一个 scene 也可能有主窗口与悬浮窗口。筛选 foregroundActive 能过滤状态,仍不能证明剩下的那个就是用户当前操作的窗口。实在需要全局兼容入口时,应限制为唯一明确候选;歧义时要求调用方传上下文,不任取一个。

8.2、附加窗口必须绑定到宿主 scene 🔼 🔽#

调试面板、提示窗、悬浮按钮、开屏等额外 UIWindow 都要检查绑定归属、生命周期与释放。给窗口设置 frame 并显示,不等于它已绑定正确 scene。附加窗口成为 key 后,不能反过来让全局“当前窗口”查询把业务导航导向调试面板。

每个需要独立显示的面板按 scene 管理;若产品明确只显示一个全局入口,则管理器要跟随明确的宿主 scene 切换和断开,不能缓存第一个 SceneDelegate 永久复用。主题切换、HUD、弹窗与页面跳转也要维持同一归属。

8.3、布局读取实际容器 🔼 🔽#

迁移 scene 并不自动完成可调整窗口的适配。子视图优先使用自己的容器 bounds、安全区与约束;需要显示环境时,取所属 window scene / traits。避免继续用 UIScreen.main.bounds 决定当前窗口宽高,也不要只靠设备型号或横竖屏判断布局。WWDC26 的 main screen 章节

更完整的历史与布局思路见屏幕适配专题。非交互外屏场景在 iOS 27 还有 scene accessory 的新要求,应按当前外屏文档专项实现,不把旧版本自动外屏 role 流程当作通用新基线。

九、验证必须覆盖启动,而不只覆盖编译 🔼 🔽#

9.1、静态与构建检查 🔼 🔽#

1、对 App target 的所有实际构建配置,核对 SDK、部署目标、INFOPLIST_FILE、manifest 和 SceneDelegate target membership。

2、用 plutil -lint 检查 plist;构建后再次检查 .app/Info.plist 中展开后的 delegate 类名与配置。生成 Info 的工程尤其不能只看源文件。

3、检查动态配置返回的 scene / delegate 类型、配置名、session role;OC 与 Swift 分别验证真实运行时类型。

4、运行相应工程的 Debug / Release 构建,再用 iOS / iPadOS 27 运行。模拟器 SDK 版本不等于模拟器运行系统版本;记录二者。

5、构建有安装依赖、导出 IPA 或其它 Build Phase 时,先确认副作用、输出位置和验证开关;不为只验证 scene 擅自覆盖已有交付包。

9.2、启动与多窗口验收矩阵 🔼 🔽#

用例预期能发现的问题
干净安装后点击图标进程初始化一次,scene 连接,出现业务根 UImanifest / 类名 / target 错误,缺窗口
保留数据覆盖升级后启动旧 session 可连接或安全回退只在新安装成功,旧会话配置失效
普通后台再回前台保留原窗口与页面,不重建 root把 active 回调误当冷启动
关闭进程后 URL Scheme 打开目标在对应 scene 就绪后出现一次只实现热 URL、开屏覆盖路由
进程存活时 URL Scheme 打开已连接目标 scene 正确导航错误使用 launchOptions、跳到其它窗口
冷 / 热 Universal Links按 activity 路由,恢复不覆盖主动入口把所有 activity 都当页面恢复
冷 / 热主屏快捷操作动作恰好执行一次,completion 正确结束漏 connectionOptions、重复处理
冷 / 热点击通知正确场景响应,不重复 push通知代理过晚注册、双入口重复消费
iPad 新建第二窗口独立 root、导航与页面状态复用全局窗口或单例页面
A 后台、B 仍前台A 的 UI 暂停,B 与共享服务继续正确工作用单个 scene 状态代表整个进程
session 断开再重连重建 UI 并恢复本 session 状态依赖配置回调重跑、代理指针缓存
关闭一个 scene只清理目标 UI / session 资源关闭所有窗口、误删共享数据
调试 / 悬浮窗口出现与消失留在宿主 scene,不抢错业务导航key window 查询被附加窗口干扰
窗口缩放、分屏、外接屏移动内容按实际容器布局固定主屏尺寸与全局方向判断

工程测试建议:在日志中记录进程启动次数、session 的 persistentIdentifier、配置名、回调名与窗口所属 scene。日志用于证明入口发生与归属,不打印 token、完整敏感 URL 参数或通知私密载荷。

9.3、通过标准与边界 🔼 🔽#

“配置正确”“语法通过”“完整编译通过”“iOS 27 能启动”“多窗口业务通过”是不同结论。只有实际执行对应步骤后才能标记通过。最低支持系统、iOS 27 / iPadOS 27、Debug / Release、干净安装 / 升级安装要记录真实覆盖范围;模拟器结果不冒充真机验收。Apple 推荐的 iPad 回归场景

十、常见失败:按启动链条定位 🔼 🔽#

现象优先检查修复方向
新 SDK 包直接断言退出最终包 manifest、配置返回值、scene 是否真正被创建补完整 scene 配置与窗口级代理
找不到 SceneDelegate 类OC / Swift 运行时名称、模块名展开、target membershipOC 普通类名与 Swift 模块类名分别配置
启动画面后黑屏willConnect 是否到达、window 是否被持有、是否绑定当前 scene、有无 root完成该 scene 的窗口装配
出现两套 root 或开屏AppDelegate 与 SceneDelegate 是否都建窗口进程启动不再建立业务主窗口
点击图标正常,冷链接无效是否解析 connectionOptions,是否被开屏 / 恢复覆盖冷热入口归一化并在 UI 就绪后消费
第二窗口操作影响第一窗口单例 root / SceneDelegate、全局 window、全局导航栈UI 状态按 session 隔离
进入后台后其它窗口断网 / 停止工作是否用单一 scene 回调关闭共享服务按进程聚合状态处理共享能力
新安装成功,覆盖安装失败旧 session 保存配置、配置名或运行时类改名增加升级测试与恢复兼容策略
调试面板出现后弹窗位置错误查询是否拿到悬浮 key window显式传入宿主业务窗口 / scene
编译通过但仍无法证明适配是否实际使用新 SDK 并运行在 27 系统补启动、入口和多窗口运行验证

十一、Jobs 三工程的落地与验证记录 🔼 🔽#

本节记录实际落地状态,与上文通用迁移方案分开维护。工程代码、依赖、工具链或测试条件改变后,需要重跑相关验证,不能把这份记录当永久编译保证。

11.1、审计结论:三工程原本已有 scene,修复的是缺口 🔼 🔽#

三工程均已具备 SceneDelegate、scene manifest、多窗口 Demo 与状态恢复基础,不属于从纯 AppDelegate 工程从零接入的情况。配置保持 UIApplicationSupportsMultipleScenes = true。OC 两工程保留已有 manifest 与动态配置回调,配置名称、代理类型保持对应;Swift UIKit 工程则移除重复回调,由 manifest 作为配置单一来源。

工程集成形态主要落地点
OC 新工程主工程与 Jobs 自建本地 Pods入口/AppDelegate、入口/SceneDelegate、MyAppTools
OC 老工程Jobs 能力直接集成主工程启动配置/AppDelegate & SceneDelegate、MyAppTools
Swift UIKit 工程UIKit 宿主、Jobs Swift 能力与 Flutter add-to-appInfo.plist、AppDelegate.swift、SceneDelegate.swift

11.2、OC 新旧工程同步修复 🔼 🔽#

  • 移除每个 SceneDelegate 对全局 scene 连接通知的监听,避免另一个 scene 连接时,已有代理也收到通知并把自己的 windowScene 引用覆盖成别人的 scene。

  • 在 willConnect 校验 UIWindowScene,使用本次连接的 scene 创建窗口;独立 root、Demo 恢复、窗口持有与显示集中于本 scene。

  • 普通 TabBar 根工厂不再返回 AppDelegate 的静态缓存。每次建立独立页面、按钮、配置、TabBar 控制器及导航控制器;自定义 TabBar 的配置与回调按实例持有。普通创建工厂分配新实例,明确的 Shared 兼容工厂继续保留共享语义,不混作普通 scene 根工厂。

  • 独立 config 还必须真正作用于实际 children:UIKit 的 super init 可能提前触发 loadView / viewDidLoad,此时子类配置尚未注入,旧流程会走全局数组兜底。装配改为首次 viewWillAppear、调用 super 之前安装本实例 children、bar 与初选状态,并提前置一次性保护;后续出现不重复重建。验证直接比较实际 children 与本实例配置,不能只比较两个 root 指针不同。

  • TabBar 按当前自身 bounds 布局,并在首次装配与布局变化时刷新。全局根类型切换只更新各 scene 代理持有且识别为普通业务 root 的窗口,不覆盖 Scene Demo 或悬浮窗。

  • Tab 根入口的真实冷启动回归暴露了设置基类的返回手势初始化问题:子控制器在 viewDidLoad 时不一定已有 navigation controller。手势代理设置移到 viewWillAppear 调用 super 之后,并校验手势非空,避免对空对象的 Block 属性继续调用。

  • OC 老工程的普通列表冷启动还暴露 JobsIconfont.m 中私有 UIGraphicsImageRendererFormat.byScale 只有声明、缺少实现。修复从 OC 新工程对应 Jobs 源码平移该 helper 实现,不新增文件或依赖;这是列表启动依赖的 missing selector,不是 scene 生命周期强制断言。

  • 将 launchFunc1 归回 AppDelegate 的进程启动流程,避免每新建一个窗口重复进行全进程初始化。

  • 开屏配置改为显式接收窗口参数;移除进程启动里的全局择窗与重试。scene 创建并持有窗口后,传入 self.window 展示开屏;由 Demo 活动或其恢复状态打开的窗口跳过开屏,不外推为所有普通 session 重连均跳过。scene 断开时释放本代理的 window 与 windowScene 引用。

  • 当前最低部署目标为 iOS 16.6,移除已不可达的旧系统 AppDelegate 窗口启动分支;旧窗口 helper 在 iOS 13 及以上拒绝创建脱离 scene 的业务窗口。

  • Demo 增加热 continueUserActivity:已有 Demo 恢复本窗口状态,非 Demo root 切换到本次活动对应的 Demo。

  • 单个 scene 进入后台时,先检查其它 scene 是否仍在前台;两份 OC 工程只在已无前台 scene 时发出全局停播通知。OC 新工程还在该聚合条件满足时记录已接入 CrashLog 的安全点;不把这一能力外推为 OC 老工程也已集成。

  • 已接入 CrashLog 的 OC 新工程在 sceneWillEnterForeground 即标记运行状态,sceneDidBecomeActive 再次幂等标记;避免系统权限弹窗让 scene 暂时停在前台 inactive 时,错误保留上次后台的安全退出标记。OC 老工程不为此新增 CrashLog 集成。

  • 真实后台回归继续核验 AppDelegate.saveContext 的业务调用链。两侧 Core Data 保存只处理已经加载且具有 store 的容器,不因进入后台强制触发懒加载;保存运行在 context 所属队列,失败记录日志。容器使用真实 JobsOCBaseConfigDemo.momd 模型名称,不再借助 appName() 推导模型名。排查同名 selector 时,要核对实际运行时 IMP,不能只看到某个分类中的 Block 声明就断定最终调用的实现。

    运行时取证确认,崩溃调用落到主 App 中的 -[NSObject(SysInfo) appName],实际返回字符串;它与 Jobs JobsDeviceInfo 的同名 Block 方法使用不同返回契约。原因是同一 selector 的分类冲突,不能归因为 SDK 改变了返回类型,也不能根据被信号处理遮蔽的线程位置误判为 HTTPServer。本轮只修正模型名称与保存路径,不修改外援 Admin 的 SysInfo 分类及其 Support 副本,不展开全库重命名。

窗口创建的实际表达式如下,依赖现有 Jobs DSL;这是方法中的节选,不是独立源码文件:

self.jobsSetWindow(UIWindow.initByScene(windowScene)).window
    .byWindowFrame(windowScene.coordinateSpace.bounds)
    .byRootViewController(restorationActivity ?
        [JobsOCSceneDelegateDemoVC.alloc
            initWithRestorationActivity:restorationActivity].navCtrl() :
        JobsOCMakeAppRootViewController())
    .byMakeKeyAndVisible();

对应源码:OC 新工程场景连接、OC 老工程场景连接。

11.3、Swift UIKit 与 Flutter 桥接 🔼 🔽#

  • manifest 显式声明 UIWindowScene,保留 Swift 模块限定代理类名;移除业务 UISceneStoryboardFile = Main,由现有纯代码管线唯一创建主窗口,防止自动 storyboard 窗口与手动窗口重复。
  • 移除 AppDelegate 的重复配置回调,scene 配置统一由 manifest 提供;修正非 Flutter 分支对系统代理方法的声明。
  • 每个 SceneDelegate 在可导入 Flutter 时独立持有 FlutterPluginSceneLifeCycleDelegate,并通过条件扩展实现 FlutterSceneLifeCycleProvider;连接、前后台、断开、URL、非 Demo activity 和快捷操作转发给桥接器。
  • 桥接器接收并缓存冷连接选项,后续 FlutterViewController 进入该 scene 的窗口时由 Flutter 关联 engine;不将同一个共享 engine 手工注册到全部 scene。
  • Demo 的热活动在本 scene 恢复;后台 / 断开时按其它前台 scene 判断全局 CrashLog 安全点;进入前台即标记运行,active 时再次幂等标记,不等待权限弹窗结束。LiveChat 引用在 scene 活跃时更新,在对应 scene 断开时清理。

现有窗口工厂的调用保留为:

window = UIWindow.jobsMake(scene: windowScene, root: homeViewController)

对应源码:Swift 场景代理、Swift AppDelegate、Swift manifest。Flutter 采用的是现有原生宿主 add-to-app 路线,不将官方“隐式 engine”模板的注册时机机械套到本工程显式创建的 engine。Flutter UIScene 迁移说明

当前原生宿主没有已定义的 URL Scheme 业务路由,因此没有凭空添加通用链接业务。Flutter 插件桥接已补入口,不代表所有第三方插件都已完成 scene 迁移;Flutter 官方也提示其多 scene 支持仍有边界。LiveChat 自身使用第三方单例窗口,本工程只修正宿主 scene 引用,不承诺多个 scene 同时独立聊天。与 UIKit 工程并列的 SwiftUI 工程已检查 App / WindowGroup 入口,保持原有 scene 管理方式,无需再手动迁移主窗口。

11.4、实际验证状态 🔼 🔽#

工程核验时间:2026-10-15 至 2026-10-16,时区 Asia/Shanghai。 官方资料核对日期仍为前言标注的 2026-10-15。构建与运行采用 Xcode 27、iOS 27 SDK、Debug arm64 模拟器目标,以及独立的 iPadOS 27 模拟器。

工程最终构建实际启动与窗口生命周期与独立状态
OC 新工程最终冻结源码构建 BUILD SUCCEEDED,退出码 0普通列表与开启 Tab 入口的真实冷启动均通过;列表 root 为导航控制器下的 ViewController_1;窗口与 scene 绑定正确真实 Settings 往返同一进程存活;未使用的 Core Data 容器保持未加载;后台安全点为 true,返回前台 inactive 时恢复 false。系统建立第二普通 scene 后,两套窗口、root、TabBar、config、实际 child、按钮与 bar 独立
OC 老工程补齐 Iconfont helper 后最终构建 BUILD SUCCEEDED,退出码 0最终包普通列表冷启动与 Tab 入口冷启动均通过;scene / window / root 绑定正确;渲染格式 byScale / byOpaque 运行时均可响应真实 Settings 往返保持同一进程,前后台 scene 回调到达,Core Data 容器始终未加载;系统建立第二普通 scene 后,两套窗口、root、TabBar、config、实际 child、按钮与 bar 独立
Swift UIKit 工程前台标记修复后的最终构建 BUILD SUCCEEDED,退出码 0冷启动进程存活;实际代理模块名正确,root 为 JobsSideDrawerVC,窗口绑定正确,Flutter scene provider 可用两个 session / 窗口独立、Demo 活动状态从 7 恢复至 11 的受控验证通过;最终前台标记修复后,真实 Settings 往返保持同一进程与 CrashLog session,后台 true → 前台 false,在权限提示造成的 inactive 阶段也正确

OC 双窗口进一步核对了“配置里的 children 与实际 children 一致”“全部按钮属于自己的 bar”“窗口绑定自己的 scene”。分别通过调试器调用两个窗口现有按钮的 clickBlock,切换到 index 2 / index 1,所属窗口切换、另一窗口不变的断言均通过。**这是现有回调的运行时调用验证,不是屏幕触点或手势点按验收。**第二个普通 scene 由系统请求并连接,检查的是实际运行中的两个窗口。

表内汇总本轮累计的针对性回归;最终单点变更后复验了受影响路径,没有对同一最终包重跑整张验收矩阵。Swift 最终单点变更为前台安全标记,针对它重跑完整增量构建与真实前后台;此前的双窗口与 Demo 活动证据仍按实际执行版本保留。OC 最后的 Tab 装配变更重新验证了真实 Tab 冷启动、两窗口实际 children 与按钮隔离。

最终构建副本与正式工作树逐字节核对通过:OC 新工程 15 个改动源码、OC 老工程 16 个改动源码、Swift 4 个源码 / 配置文件;三仓差异空白检查通过。采用隔离构建副本,不把临时构建目录当正式源码交付。最终成功构建日志分别为 oc-new-frozen-final-build.log、oc-old-complete-final-build.log、swift-build-arm64-final.log,中间构建结果不替代最终结果。

仍需保留以下边界:

  • Swift 首轮通用双架构构建在 Flutter debug_unpack_ios 脚本失败:脚本把 arm64 x86_64 作为单一架构识别。本轮没有修改外援或生成脚本,随后使用 arm64 完成验证;未验证 fat 双架构包、Release、真机或全部最低支持系统。
  • 启动截图仍包含工程已有开屏与系统通知授权提示。对象绑定与进程存活验证通过,不代表无遮挡业务根列表的完整操作体验已经逐项验收。
  • 原生宿主尚无已定义的入站 URL / 快捷操作业务。本轮没有实际完成所有冷 / 热 URL、Universal Links、通知点击、第三方登录 / 支付等端到端业务验收;Flutter provider 已连接也不等于所有插件都兼容多窗口。
  • Demo 的受控活动恢复不等于完整系统状态恢复验收;所有历史 session 升级兼容、用户关闭与系统回收重连、外接屏、窗口缩放、旋转、完整开屏交互与全业务流程仍按验收矩阵逐项补测。
  • SwiftUI 工程只核对既有 App / WindowGroup 入口已采用 scene,不把该只读检查记作重新构建或运行验收。

十二、官方资料与继续阅读 🔼 🔽#

资料用途
Transitioning to the UIKit scene-based life cycle当前强制门槛、职责迁移与 iOS 27 场景变化
TN3187警告沿革、迁移排查与基础配置
WWDC26:Modernize your UIKit app2:10 生命周期门槛、main screen 与可调整窗口
WWDC25:Make your UIKit app more flexiblescene 的职责、独立状态与多窗口可选边界
About the app launch sequencemain、UIApplicationMain、launch 回调与 scene 连接顺序
Responding to the launch of your app进程初始化、连接原因与 launchOptions 变化
Specifying the scenes your app supportsmanifest、静态与动态配置、storyboard
UISceneDelegate / UIWindowSceneDelegate生命周期、URL、活动与快捷操作入口
Restoring your app’s state按 scene 保存恢复状态与测试方式
SwiftUI App / UIApplicationDelegateAdaptorSwiftUI 入口与 UIKit 代理桥接

同目录专题:iOS 屏幕适配专题 · 配置 Info.plist

我是有底线的➤点我回到首页