capacitor-uiscene-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Capacitor UIScene Migration (8.4 -> 8.5)

Capacitor UIScene 迁移(8.4 -> 8.5)

Capacitor 8.5 moved the iOS template to the UIScene lifecycle because Xcode 27 requires it. It shipped as a breaking minor, iOS only. The 8.5 core still runs the AppDelegate path, so bumping the dependency alone does not break an app; the app project must adopt scenes before it can build with Xcode 27 (and before Capacitor 9).
Capacitor 8.5 将 iOS 模板迁移到了 UIScene 生命周期,因为 Xcode 27 要求这样做。这以破坏性次版本的形式发布,仅影响 iOS。8.5 core 仍会运行 AppDelegate 路径,因此仅升级依赖并不会导致应用损坏;应用项目必须先采用 scene,之后才能使用 Xcode 27 构建(也才能升级到 Capacitor 9)。

When to Use

何时使用

TRIGGER when:
  • App is on
    @capacitor/ios
    8.0-8.4, or on 8.5 without
    UIApplicationSceneManifest
    in
    Info.plist
  • Xcode 27 build or runtime warning "CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle"
  • npx cap migrate
    printed
    UIScene migration: project is in a partial state ... Skipping automated migration
  • After adding a scene manifest: custom URL scheme, universal link,
    appUrlOpen
    , or AppDelegate
    applicationDidBecomeActive
    code stopped running
  • Build error on
    tmpWindow
    or
    TmpViewController
    after updating to 8.5
  • A plugin author asks whether their iOS code survives the scene lifecycle
  • Preparing a Capacitor 8 -> 9 upgrade from 8.4 or earlier
Do not use:
  • Capacitor 8 -> 9 package/native upgrade itself ->
    capacitor-app-upgrade-v8-to-v9
    /
    capacitor-plugin-upgrade-v8-to-v9
  • Setting up deep links from scratch ->
    capacitor-deep-linking
  • Android (UIScene is iOS only)
  • Multi-window or resizable layout work beyond lifecycle adoption
在以下情况下触发:
  • 应用使用
    @capacitor/ios
    8.0-8.4,或已升级到 8.5,但
    Info.plist
    中没有
    UIApplicationSceneManifest
  • Xcode 27 在构建或运行时发出警告:“CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle”
  • npx cap migrate
    打印了
    UIScene migration: project is in a partial state ... Skipping automated migration
  • 添加 scene manifest 后,自定义 URL scheme、Universal Link、
    appUrlOpen
    或 AppDelegate 的
    applicationDidBecomeActive
    代码停止运行
  • 升级到 8.5 后出现与
    tmpWindow
    或
    TmpViewController
    有关的构建错误
  • 插件作者询问其 iOS 代码能否继续在 scene 生命周期下运行
  • 准备从 8.4 或更早版本升级到 Capacitor 8 -> 9
不适用于:
  • Capacitor 8 -> 9 的 package/native 升级本身 ->
    capacitor-app-upgrade-v8-to-v9
    /
    capacitor-plugin-upgrade-v8-to-v9
  • 从头设置 deep link ->
    capacitor-deep-linking
  • Android(UIScene 仅适用于 iOS)
  • 超出生命周期迁移范围的多窗口或可调整大小布局工作

Live Project Snapshot

项目实时快照

!
node -e "const fs=require('fs');if(!fs.existsSync('package.json'))process.exit(0);const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const out=[];for(const s of ['dependencies','devDependencies','peerDependencies']){for(const [n,v] of Object.entries(pkg[s]||{})){if(n.startsWith('@capacitor/'))out.push(s+'.'+n+'='+v)}}const p='ios/App/App/';out.push('SceneDelegate.swift='+fs.existsSync(p+'SceneDelegate.swift'));if(fs.existsSync(p+'Info.plist'))out.push('UIApplicationSceneManifest='+fs.readFileSync(p+'Info.plist','utf8').includes('UIApplicationSceneManifest'));if(fs.existsSync(p+'AppDelegate.swift'))out.push('configurationForConnecting='+fs.readFileSync(p+'AppDelegate.swift','utf8').includes('UISceneConfiguration(name:'));console.log(out.join('\n'))"
!
node -e "const fs=require('fs');if(!fs.existsSync('package.json'))process.exit(0);const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const out=[];for(const s of ['dependencies','devDependencies','peerDependencies']){for(const [n,v] of Object.entries(pkg[s]||{})){if(n.startsWith('@capacitor/'))out.push(s+'.'+n+'='+v)}}const p='ios/App/App/';out.push('SceneDelegate.swift='+fs.existsSync(p+'SceneDelegate.swift'));if(fs.existsSync(p+'Info.plist'))out.push('UIApplicationSceneManifest='+fs.readFileSync(p+'Info.plist','utf8').includes('UIApplicationSceneManifest'));if(fs.existsSync(p+'AppDelegate.swift'))out.push('configurationForConnecting='+fs.readFileSync(p+'AppDelegate.swift','utf8').includes('UISceneConfiguration(name:'));console.log(out.join('\n'))}

Facts the Audit Depends On

审计所依据的事实

  • Once
    UIApplicationSceneManifest
    exists, iOS stops calling these AppDelegate methods:
    application(_:open:options:)
    ,
    application(_:continue:restorationHandler:)
    ,
    applicationDidBecomeActive
    ,
    applicationWillResignActive
    ,
    applicationDidEnterBackground
    ,
    applicationWillEnterForeground
    . Code in them silently stops running. No compiler warning.
  • Still called on the AppDelegate:
    didFinishLaunchingWithOptions
    ,
    applicationWillTerminate
    , push token registration (
    didRegisterForRemoteNotificationsWithDeviceToken
    /
    didFailToRegister...
    ) and remote-notification delivery.
    UIApplication.*Notification
    lifecycle notifications still fire.
  • SceneDelegateProxy
    (
    CAPSceneDelegateProxy.swift
    ) re-posts
    .capacitorOpenURL
    ,
    .capacitorOpenUniversalLink
    (same payload) and
    CDVPluginHandleOpenURL
    , and mirrors the URL into
    ApplicationDelegateProxy.shared.lastURL
    . Cold-start URLs and user activities are held until the first
    .capacitorViewDidAppear
    , so plugins see them after registration and
    App.getLaunchUrl()
    works.
  • New 8.5-only notifications:
    .capacitorSceneWillConnect
    ,
    .capacitorSceneOpenURL
    ,
    .capacitorSceneOpenUniversalLink
    (object =
    UIScene
    , payload in
    userInfo
    ). Not posted on 8.4.
  • JS
    pause
    /
    resume
    are driven by
    UIScene.didEnterBackgroundNotification
    /
    willEnterForegroundNotification
    , filtered to the bridge's scene.
  • Removed in 8.5:
    TmpViewController
    ,
    CapacitorBridge.tmpWindow
    ,
    tmpViewControllerAppeared
    . Any reference is a build error; present from
    bridge.viewController
    .
  • The 8.5 SceneDelegate creates the window and
    CAPBridgeViewController()
    in code. A custom bridge subclass set in
    Main.storyboard
    must be instantiated in the SceneDelegate instead.
  • cap migrate
    (CLI 8.5+) classifies the project by three signals: manifest in
    Info.plist
    ,
    SceneDelegate.swift
    on disk,
    UISceneConfiguration(name:
    in
    AppDelegate.swift
    . 0/3 -> migrates; 3/3 -> skips as done; 1-2/3 -> warns "partial state" and does nothing. It never overwrites an existing
    SceneDelegate.swift
    . It warns (does not fix) on
    UIApplication.shared.applicationState
    ,
    tmpWindow
    ,
    TmpViewController
    , and custom
    open url:
    /
    continue userActivity:
    bodies.
  • 一旦存在
    UIApplicationSceneManifest
    ,iOS 就会停止调用以下 AppDelegate 方法:
    application(_:open:options:)
    、
    application(_:continue:restorationHandler:)
    、
    applicationDidBecomeActive
    、
    applicationWillResignActive
    、
    applicationDidEnterBackground
    、
    applicationWillEnterForeground
    。这些方法中的代码会悄无声息地停止运行,不会产生编译器警告。
  • AppDelegate 上仍会调用:
    didFinishLaunchingWithOptions
    、
    applicationWillTerminate
    、push token 注册(
    didRegisterForRemoteNotificationsWithDeviceToken
    /
    didFailToRegister...
    )以及远程通知传递。
    UIApplication.*Notification
    生命周期通知仍会触发。
  • SceneDelegateProxy
    (
    CAPSceneDelegateProxy.swift
    )会重新发布
    .capacitorOpenURL
    、
    .capacitorOpenUniversalLink
    (payload 相同)和
    CDVPluginHandleOpenURL
    ,并将该 URL 镜像到
    ApplicationDelegateProxy.shared.lastURL
    。冷启动 URL 和 user activity 会暂存到首次出现
    .capacitorViewDidAppear
    为止,因此插件能够在注册后看到它们,且
    App.getLaunchUrl()
    可以正常工作。
  • 8.5 新增的专属通知:
    .capacitorSceneWillConnect
    、
    .capacitorSceneOpenURL
    、
    .capacitorSceneOpenUniversalLink
    (object =
    UIScene
    ,payload 位于
    userInfo
    中)。这些通知不会在 8.4 中发布。
  • JS 的
    pause
    /
    resume
    由
    UIScene.didEnterBackgroundNotification
    /
    willEnterForegroundNotification
    驱动,并会过滤到 bridge 所属的 scene。
  • 8.5 中已移除:
    TmpViewController
    、
    CapacitorBridge.tmpWindow
    、
    tmpViewControllerAppeared
    。任何相关引用都会导致构建错误;应通过
    bridge.viewController
    获取对应对象。
  • 8.5 的 SceneDelegate 通过代码创建 window 和
    CAPBridgeViewController()
    。如果在
    Main.storyboard
    中设置了自定义 bridge subclass,则必须改为在 SceneDelegate 中实例化。
  • cap migrate
    (CLI 8.5+)根据三项信号对项目进行分类:
    Info.plist
    中的 manifest、磁盘上的
    SceneDelegate.swift
    ,以及
    AppDelegate.swift
    中的
    UISceneConfiguration(name:
    。0/3 -> 执行迁移;3/3 -> 视为已完成并跳过;1-2/3 -> 警告“partial state”且不执行任何操作。它绝不会覆盖现有的
    SceneDelegate.swift
    。对于
    UIApplication.shared.applicationState
    、
    tmpWindow
    、
    TmpViewController
    以及自定义的
    open url:
    /
    continue userActivity:
    方法体,它只会发出警告,不会自动修复。

Procedure

操作流程

Audit, report, ask, then edit. Never edit before the user has seen the findings.
  1. Detect repo type.
    ios/App/App.xcodeproj
    -> app.
    Package.swift
    /
    *.podspec
    with
    CAPPlugin
    sources and no app project -> plugin; go to step 9.
  2. Check versions and package manager.
    @capacitor/ios
    must reach
    ^8.5.0
    .
    ios/App/Podfile
    -> CocoaPods;
    ios/App/CapApp-SPM
    -> SPM. The SceneDelegate is identical for both.
  3. Classify with the snapshot's three signals (eligible / migrated / partial).
  4. Audit with the commands in
    references/audit-patterns.md
    , covering
    ios/App
    and installed plugins in
    node_modules
    . Record file:line for each hit.
  5. Report grouped as: build blockers (
    tmpWindow
    ,
    TmpViewController
    ), needs a decision (custom URL/activity/lifecycle bodies, existing SceneDelegate, storyboard custom class, third-party SDK forwarding), informational (observers that keep working,
    applicationState
    , issues inside
    node_modules
    ).
  6. Ask at the real judgement calls:
    • Custom body in
      open url:
      /
      continue userActivity:
      : move it into
      scene(_:openURLContexts:)
      /
      scene(_:continue:)
      next to the proxy call (show the body first).
    • Custom lifecycle code: move to
      sceneDidBecomeActive
      /
      sceneWillResignActive
      /
      sceneDidEnterBackground
      /
      sceneWillEnterForeground
      , or to
      UIApplication
      notification observers.
    • Remove the now-dead AppDelegate URL methods, or keep them (harmless).
    • SDKs that need URL forwarding (Facebook, Google Sign-In, OAuth redirect libraries): check their docs for a scene-delegate entry point and call it from the SceneDelegate.
  7. Apply.
    • Eligible and template-shaped:
      npm i @capacitor/core@^8.5.0 @capacitor/ios@^8.5.0
      ,
      npm i -D @capacitor/cli@^8.5.0
      , then
      npx cap migrate
      . Read every log line; anything skipped is handled by hand for that step only. Then apply step 6 decisions.
    • Partial or customized: merge only the missing pieces using
      references/scene-delegate-template.md
      . Keep existing keys, methods and custom logic. Register the new file per
      references/pbxproj-registration.md
      .
  8. Sync and build:
    npx cap sync ios
    , then build (see Verification). Re-check the three signals: all must be true.
  9. Plugin repos: audit and advise only, using
    references/plugin-audit.md
    . Never touch app files from a plugin repo; edit plugin sources only when the user asks.
先审计、报告、询问,然后编辑。在用户看到审计结果之前,绝不进行任何编辑。
  1. 识别仓库类型。
    ios/App/App.xcodeproj
    -> app。包含
    CAPPlugin
    源码但没有 app 项目的
    Package.swift
    /
    *.podspec
    -> plugin;转到步骤 9。
  2. 检查版本和包管理器。
    @capacitor/ios
    必须升级到
    ^8.5.0
    。存在
    ios/App/Podfile
    -> CocoaPods;存在
    ios/App/CapApp-SPM
    -> SPM。两种方式使用的 SceneDelegate 完全相同。
  3. 使用快照中的三项信号进行分类(eligible / migrated / partial)。
  4. 使用
    references/audit-patterns.md
    中的命令进行审计,范围包括
    ios/App
    和
    node_modules
    中已安装的插件。记录每个命中项的 file:line。
  5. 报告按以下类别分组:构建阻塞项(
    tmpWindow
    、
    TmpViewController
    )、需要决策的项(自定义 URL/activity/生命周期方法体、现有 SceneDelegate、storyboard 自定义 class、第三方 SDK 转发)、信息性项目(仍可工作的观察器、
    applicationState
    、
    node_modules
    内部的问题)。
  6. 在确实需要判断的节点询问:
    • open url:
      /
      continue userActivity:
      中存在自定义方法体:将其迁移到
      scene(_:openURLContexts:)
      /
      scene(_:continue:)
      中 proxy 调用附近的位置(先展示原方法体)。
    • 存在自定义生命周期代码:将其迁移到
      sceneDidBecomeActive
      /
      sceneWillResignActive
      /
      sceneDidEnterBackground
      /
      sceneWillEnterForeground
      ,或迁移到
      UIApplication
      通知观察器中。
    • 移除已经失效的 AppDelegate URL 方法,或继续保留(不会造成影响)。
    • 需要 URL 转发的 SDK(Facebook、Google Sign-In、OAuth redirect 库):查阅其文档以确认是否提供 scene-delegate 入口,并从 SceneDelegate 中调用该入口。
  7. 应用变更。
    • Eligible 且为模板结构:执行
      npm i @capacitor/core@^8.5.0 @capacitor/ios@^8.5.0
      、
      npm i -D @capacitor/cli@^8.5.0
      ,然后运行
      npx cap migrate
      。逐行阅读日志;任何被跳过的内容都需要仅针对该步骤手动处理。然后应用步骤 6 中的决策。
    • Partial 或已定制:使用
      references/scene-delegate-template.md
      仅合并缺失部分。保留现有 key、方法和自定义逻辑。按照
      references/pbxproj-registration.md
      注册新文件。
  8. 同步并构建:运行
    npx cap sync ios
    ,然后构建(参见“验证”)。重新检查三项信号:必须全部为 true。
  9. Plugin 仓库:仅进行审计并提供建议,使用
    references/plugin-audit.md
    。绝不修改 plugin 仓库中的 app 文件;仅在用户明确要求时才修改 plugin 源码。

Verification

验证

bash
npx cap sync ios
xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Debug \
  -destination 'generic/platform=iOS Simulator' build   # use -workspace App.xcworkspace for CocoaPods
grep -rn --include=*.swift -E '\btmpWindow\b|\bTmpViewController\b' ios/App   # expect nothing
On a device or simulator:
  • App launches to the web view (no black screen).
  • Background then foreground: JS
    pause
    and
    resume
    fire once each.
  • Custom scheme, warm (app running):
    appUrlOpen
    fires. Use
    xcrun simctl openurl booted myscheme://test
    .
  • Custom scheme, cold (app killed, then link):
    appUrlOpen
    fires and
    App.getLaunchUrl()
    returns the URL.
  • Universal link routes in, if the app has associated domains (needs a real domain and AASA file).
  • Push: token still arrives (
    registration
    event), and a tapped notification opens the app.
  • Any moved custom logic (analytics on foreground, SDK URL handlers) runs.
bash
npx cap sync ios
xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Debug \
  -destination 'generic/platform=iOS Simulator' build   # use -workspace App.xcworkspace for CocoaPods
grep -rn --include=*.swift -E '\btmpWindow\b|\bTmpViewController\b' ios/App   # expect nothing
在真机或模拟器上验证:
  • 应用启动后显示 web view(没有黑屏)。
  • 从后台切换到前台:JS 的
    pause
    和
    resume
    各触发一次。
  • 自定义 scheme,热启动(应用正在运行):
    appUrlOpen
    触发。使用
    xcrun simctl openurl booted myscheme://test
    。
  • 自定义 scheme,冷启动(先终止应用,再打开链接):
    appUrlOpen
    触发,且
    App.getLaunchUrl()
    返回该 URL。
  • 如果应用配置了关联域名,Universal Link 可以路由进入(需要真实域名和 AASA 文件)。
  • Push:token 仍能到达(触发
    registration
    事件),点击通知可以打开应用。
  • 所有迁移后的自定义逻辑(进入前台时的 analytics、SDK URL handler)均正常运行。

Error Handling

错误处理

  • cap migrate
    says "partial state" -> expected for hand-edited projects; merge by hand (step 7). Do not reset files without asking.
  • Black screen after migration -> SceneDelegate does not create a window, or the
    UISceneDelegateClassName
    in
    Info.plist
    does not match the class. Use
    $(PRODUCT_MODULE_NAME).SceneDelegate
    and the template window setup.
  • Cannot find 'SceneDelegate' in scope
    in AppDelegate -> file not in the App target; see
    references/pbxproj-registration.md
    .
  • Value of type 'CapacitorBridge' has no member 'tmpWindow'
    -> delete the reference; present from
    bridge?.viewController
    .
  • Deep link works warm but not cold -> URL logic still lives in the AppDelegate, or the app reads it before the bridge appears; rely on
    appUrlOpen
    +
    getLaunchUrl()
    .
  • Custom bridge subclass not used (plugins registered in
    capacitorDidLoad
    missing) -> instantiate the subclass in
    scene(_:willConnectTo:)
    .
  • pbxproj
    hand edit makes Xcode refuse to open the project -> revert and add the file through Xcode.
  • cap migrate
    提示“partial state”-> 对于手动编辑过的项目,这是预期情况;按步骤 7 手动合并。不要在未经询问的情况下重置文件。
  • 迁移后出现黑屏 -> SceneDelegate 没有创建 window,或者
    Info.plist
    中的
    UISceneDelegateClassName
    与 class 不匹配。使用
    $(PRODUCT_MODULE_NAME).SceneDelegate
    和模板中的 window 设置。
  • AppDelegate 中出现“Cannot find 'SceneDelegate' in scope”-> 该文件未加入 App target;参见
    references/pbxproj-registration.md
    。
  • “Value of type 'CapacitorBridge' has no member 'tmpWindow'”-> 删除该引用;通过
    bridge?.viewController
    获取对应对象。
  • Deep link 热启动正常但冷启动失败 -> URL 逻辑仍位于 AppDelegate 中,或者应用在 bridge 出现之前读取了 URL;依靠
    appUrlOpen
    +
    getLaunchUrl()
    。
  • 自定义 bridge subclass 未生效(在
    capacitorDidLoad
    中注册的插件缺失)-> 在
    scene(_:willConnectTo:)
    中实例化该 subclass。
  • 手动编辑
    pbxproj
    后 Xcode 无法打开项目 -> 回退更改,然后通过 Xcode 添加文件。

References

参考资料

Only load a reference when its topic is in play.
FileLoad when
references/audit-patterns.md
Step 4: grep commands and how to classify each hit
references/scene-delegate-template.md
Creating or merging SceneDelegate, Info.plist manifest, AppDelegate hook, custom subclass and lifecycle variants
references/pbxproj-registration.md
Registering
SceneDelegate.swift
without Xcode, or verifying the CLI did
references/plugin-audit.md
The repo is a plugin, or an installed plugin shows lifecycle hits
Related skills:
capacitor-app-upgrade-v8-to-v9
,
capacitor-plugin-upgrade-v8-to-v9
,
capacitor-app-upgrades
,
capacitor-deep-linking
,
capacitor-push-notifications
.
仅在相关主题涉及时加载对应的 reference。
文件何时加载
references/audit-patterns.md
步骤 4:grep 命令及如何对每个命中项进行分类
references/scene-delegate-template.md
创建或合并 SceneDelegate、Info.plist manifest、AppDelegate hook、自定义 subclass 和生命周期变体
references/pbxproj-registration.md
不通过 Xcode 注册 SceneDelegate.swift,或验证 CLI 是否已执行注册
references/plugin-audit.md
仓库为 plugin,或已安装的 plugin 出现生命周期相关命中项
相关 skills:
capacitor-app-upgrade-v8-to-v9
、
capacitor-plugin-upgrade-v8-to-v9
、
capacitor-app-upgrades
、
capacitor-deep-linking
、
capacitor-push-notifications
。