capacitor-uiscene-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCapacitor 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).
Canonical source: https://capacitorjs.com/docs/updating/8-5
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 8.0-8.4, or on 8.5 without
@capacitor/iosinUIApplicationSceneManifestInfo.plist - Xcode 27 build or runtime warning "CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle"
- printed
npx cap migrateUIScene migration: project is in a partial state ... Skipping automated migration - After adding a scene manifest: custom URL scheme, universal link, , or AppDelegate
appUrlOpencode stopped runningapplicationDidBecomeActive - Build error on or
tmpWindowafter updating to 8.5TmpViewController - 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-v9capacitor-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
在以下情况下触发:
- 应用使用 8.0-8.4,或已升级到 8.5,但
@capacitor/ios中没有Info.plistUIApplicationSceneManifest - Xcode 27 在构建或运行时发出警告:“CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle”
- 打印了
npx cap migrateUIScene migration: project is in a partial state ... Skipping automated migration - 添加 scene manifest 后,自定义 URL scheme、Universal Link、或 AppDelegate 的
appUrlOpen代码停止运行applicationDidBecomeActive - 升级到 8.5 后出现与 或
tmpWindow有关的构建错误TmpViewController - 插件作者询问其 iOS 代码能否继续在 scene 生命周期下运行
- 准备从 8.4 或更早版本升级到 Capacitor 8 -> 9
不适用于:
- Capacitor 8 -> 9 的 package/native 升级本身 -> /
capacitor-app-upgrade-v8-to-v9capacitor-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 exists, iOS stops calling these AppDelegate methods:
UIApplicationSceneManifest,application(_:open:options:),application(_:continue:restorationHandler:),applicationDidBecomeActive,applicationWillResignActive,applicationDidEnterBackground. Code in them silently stops running. No compiler warning.applicationWillEnterForeground - Still called on the AppDelegate: ,
didFinishLaunchingWithOptions, push token registration (applicationWillTerminate/didRegisterForRemoteNotificationsWithDeviceToken) and remote-notification delivery.didFailToRegister...lifecycle notifications still fire.UIApplication.*Notification - (
SceneDelegateProxy) re-postsCAPSceneDelegateProxy.swift,.capacitorOpenURL(same payload) and.capacitorOpenUniversalLink, and mirrors the URL intoCDVPluginHandleOpenURL. Cold-start URLs and user activities are held until the firstApplicationDelegateProxy.shared.lastURL, so plugins see them after registration and.capacitorViewDidAppearworks.App.getLaunchUrl() - New 8.5-only notifications: ,
.capacitorSceneWillConnect,.capacitorSceneOpenURL(object =.capacitorSceneOpenUniversalLink, payload inUIScene). Not posted on 8.4.userInfo - JS /
pauseare driven byresume/UIScene.didEnterBackgroundNotification, filtered to the bridge's scene.willEnterForegroundNotification - Removed in 8.5: ,
TmpViewController,CapacitorBridge.tmpWindow. Any reference is a build error; present fromtmpViewControllerAppeared.bridge.viewController - The 8.5 SceneDelegate creates the window and in code. A custom bridge subclass set in
CAPBridgeViewController()must be instantiated in the SceneDelegate instead.Main.storyboard - (CLI 8.5+) classifies the project by three signals: manifest in
cap migrate,Info.pliston disk,SceneDelegate.swiftinUISceneConfiguration(name:. 0/3 -> migrates; 3/3 -> skips as done; 1-2/3 -> warns "partial state" and does nothing. It never overwrites an existingAppDelegate.swift. It warns (does not fix) onSceneDelegate.swift,UIApplication.shared.applicationState,tmpWindow, and customTmpViewController/open url:bodies.continue userActivity:
- 一旦存在 ,iOS 就会停止调用以下 AppDelegate 方法:
UIApplicationSceneManifest、application(_:open:options:)、application(_:continue:restorationHandler:)、applicationDidBecomeActive、applicationWillResignActive、applicationDidEnterBackground。这些方法中的代码会悄无声息地停止运行,不会产生编译器警告。applicationWillEnterForeground - AppDelegate 上仍会调用:、
didFinishLaunchingWithOptions、push token 注册(applicationWillTerminate/didRegisterForRemoteNotificationsWithDeviceToken)以及远程通知传递。didFailToRegister...生命周期通知仍会触发。UIApplication.*Notification - (
SceneDelegateProxy)会重新发布CAPSceneDelegateProxy.swift、.capacitorOpenURL(payload 相同)和.capacitorOpenUniversalLink,并将该 URL 镜像到CDVPluginHandleOpenURL。冷启动 URL 和 user activity 会暂存到首次出现ApplicationDelegateProxy.shared.lastURL为止,因此插件能够在注册后看到它们,且.capacitorViewDidAppear可以正常工作。App.getLaunchUrl() - 8.5 新增的专属通知:、
.capacitorSceneWillConnect、.capacitorSceneOpenURL(object =.capacitorSceneOpenUniversalLink,payload 位于UIScene中)。这些通知不会在 8.4 中发布。userInfo - JS 的 /
pause由resume/UIScene.didEnterBackgroundNotification驱动,并会过滤到 bridge 所属的 scene。willEnterForegroundNotification - 8.5 中已移除:、
TmpViewController、CapacitorBridge.tmpWindow。任何相关引用都会导致构建错误;应通过tmpViewControllerAppeared获取对应对象。bridge.viewController - 8.5 的 SceneDelegate 通过代码创建 window 和 。如果在
CAPBridgeViewController()中设置了自定义 bridge subclass,则必须改为在 SceneDelegate 中实例化。Main.storyboard - (CLI 8.5+)根据三项信号对项目进行分类:
cap migrate中的 manifest、磁盘上的Info.plist,以及SceneDelegate.swift中的AppDelegate.swift。0/3 -> 执行迁移;3/3 -> 视为已完成并跳过;1-2/3 -> 警告“partial state”且不执行任何操作。它绝不会覆盖现有的UISceneConfiguration(name:。对于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.
- Detect repo type. -> app.
ios/App/App.xcodeproj/Package.swiftwith*.podspecsources and no app project -> plugin; go to step 9.CAPPlugin - Check versions and package manager. must reach
@capacitor/ios.^8.5.0-> CocoaPods;ios/App/Podfile-> SPM. The SceneDelegate is identical for both.ios/App/CapApp-SPM - Classify with the snapshot's three signals (eligible / migrated / partial).
- Audit with the commands in , covering
references/audit-patterns.mdand installed plugins inios/App. Record file:line for each hit.node_modules - Report grouped as: build blockers (,
tmpWindow), needs a decision (custom URL/activity/lifecycle bodies, existing SceneDelegate, storyboard custom class, third-party SDK forwarding), informational (observers that keep working,TmpViewController, issues insideapplicationState).node_modules - Ask at the real judgement calls:
- Custom body in /
open url:: move it intocontinue userActivity:/scene(_:openURLContexts:)next to the proxy call (show the body first).scene(_:continue:) - Custom lifecycle code: move to /
sceneDidBecomeActive/sceneWillResignActive/sceneDidEnterBackground, or tosceneWillEnterForegroundnotification observers.UIApplication - 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.
- Custom body in
- Apply.
- Eligible and template-shaped: ,
npm i @capacitor/core@^8.5.0 @capacitor/ios@^8.5.0, thennpm i -D @capacitor/cli@^8.5.0. Read every log line; anything skipped is handled by hand for that step only. Then apply step 6 decisions.npx cap migrate - Partial or customized: merge only the missing pieces using . Keep existing keys, methods and custom logic. Register the new file per
references/scene-delegate-template.md.references/pbxproj-registration.md
- Eligible and template-shaped:
- Sync and build: , then build (see Verification). Re-check the three signals: all must be true.
npx cap sync ios - Plugin repos: audit and advise only, using . Never touch app files from a plugin repo; edit plugin sources only when the user asks.
references/plugin-audit.md
先审计、报告、询问,然后编辑。在用户看到审计结果之前,绝不进行任何编辑。
- 识别仓库类型。 -> app。包含
ios/App/App.xcodeproj源码但没有 app 项目的CAPPlugin/Package.swift-> plugin;转到步骤 9。*.podspec - 检查版本和包管理器。 必须升级到
@capacitor/ios。存在^8.5.0-> CocoaPods;存在ios/App/Podfile-> SPM。两种方式使用的 SceneDelegate 完全相同。ios/App/CapApp-SPM - 使用快照中的三项信号进行分类(eligible / migrated / partial)。
- 使用 中的命令进行审计,范围包括
references/audit-patterns.md和ios/App中已安装的插件。记录每个命中项的 file:line。node_modules - 报告按以下类别分组:构建阻塞项(、
tmpWindow)、需要决策的项(自定义 URL/activity/生命周期方法体、现有 SceneDelegate、storyboard 自定义 class、第三方 SDK 转发)、信息性项目(仍可工作的观察器、TmpViewController、applicationState内部的问题)。node_modules - 在确实需要判断的节点询问:
- /
open url:中存在自定义方法体:将其迁移到continue userActivity:/scene(_:openURLContexts:)中 proxy 调用附近的位置(先展示原方法体)。scene(_:continue:) - 存在自定义生命周期代码:将其迁移到 /
sceneDidBecomeActive/sceneWillResignActive/sceneDidEnterBackground,或迁移到sceneWillEnterForeground通知观察器中。UIApplication - 移除已经失效的 AppDelegate URL 方法,或继续保留(不会造成影响)。
- 需要 URL 转发的 SDK(Facebook、Google Sign-In、OAuth redirect 库):查阅其文档以确认是否提供 scene-delegate 入口,并从 SceneDelegate 中调用该入口。
- 应用变更。
- Eligible 且为模板结构:执行 、
npm i @capacitor/core@^8.5.0 @capacitor/ios@^8.5.0,然后运行npm i -D @capacitor/cli@^8.5.0。逐行阅读日志;任何被跳过的内容都需要仅针对该步骤手动处理。然后应用步骤 6 中的决策。npx cap migrate - Partial 或已定制:使用 仅合并缺失部分。保留现有 key、方法和自定义逻辑。按照
references/scene-delegate-template.md注册新文件。references/pbxproj-registration.md
- Eligible 且为模板结构:执行
- 同步并构建:运行 ,然后构建(参见“验证”)。重新检查三项信号:必须全部为 true。
npx cap sync ios - Plugin 仓库:仅进行审计并提供建议,使用 。绝不修改 plugin 仓库中的 app 文件;仅在用户明确要求时才修改 plugin 源码。
references/plugin-audit.md
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 nothingOn a device or simulator:
- App launches to the web view (no black screen).
- Background then foreground: JS and
pausefire once each.resume - Custom scheme, warm (app running): fires. Use
appUrlOpen.xcrun simctl openurl booted myscheme://test - Custom scheme, cold (app killed, then link): fires and
appUrlOpenreturns the URL.App.getLaunchUrl() - Universal link routes in, if the app has associated domains (needs a real domain and AASA file).
- Push: token still arrives (event), and a tapped notification opens the app.
registration - 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返回该 URL。App.getLaunchUrl() - 如果应用配置了关联域名,Universal Link 可以路由进入(需要真实域名和 AASA 文件)。
- Push:token 仍能到达(触发 事件),点击通知可以打开应用。
registration - 所有迁移后的自定义逻辑(进入前台时的 analytics、SDK URL handler)均正常运行。
Error Handling
错误处理
- says "partial state" -> expected for hand-edited projects; merge by hand (step 7). Do not reset files without asking.
cap migrate - Black screen after migration -> SceneDelegate does not create a window, or the in
UISceneDelegateClassNamedoes not match the class. UseInfo.plistand the template window setup.$(PRODUCT_MODULE_NAME).SceneDelegate - in AppDelegate -> file not in the App target; see
Cannot find 'SceneDelegate' in scope.references/pbxproj-registration.md - -> delete the reference; present from
Value of type 'CapacitorBridge' has no member 'tmpWindow'.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 missing) -> instantiate the subclass in
capacitorDidLoad.scene(_:willConnectTo:) - hand edit makes Xcode refuse to open the project -> revert and add the file through Xcode.
pbxproj
- 提示“partial state”-> 对于手动编辑过的项目,这是预期情况;按步骤 7 手动合并。不要在未经询问的情况下重置文件。
cap migrate - 迁移后出现黑屏 -> SceneDelegate 没有创建 window,或者 中的
Info.plist与 class 不匹配。使用UISceneDelegateClassName和模板中的 window 设置。$(PRODUCT_MODULE_NAME).SceneDelegate - 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中实例化该 subclass。scene(_:willConnectTo:) - 手动编辑 后 Xcode 无法打开项目 -> 回退更改,然后通过 Xcode 添加文件。
pbxproj
References
参考资料
Only load a reference when its topic is in play.
| File | Load when |
|---|---|
| Step 4: grep commands and how to classify each hit |
| Creating or merging SceneDelegate, Info.plist manifest, AppDelegate hook, custom subclass and lifecycle variants |
| Registering |
| The repo is a plugin, or an installed plugin shows lifecycle hits |
Related skills: , , , , .
capacitor-app-upgrade-v8-to-v9capacitor-plugin-upgrade-v8-to-v9capacitor-app-upgradescapacitor-deep-linkingcapacitor-push-notifications仅在相关主题涉及时加载对应的 reference。
| 文件 | 何时加载 |
|---|---|
| 步骤 4:grep 命令及如何对每个命中项进行分类 |
| 创建或合并 SceneDelegate、Info.plist manifest、AppDelegate hook、自定义 subclass 和生命周期变体 |
| 不通过 Xcode 注册 SceneDelegate.swift,或验证 CLI 是否已执行注册 |
| 仓库为 plugin,或已安装的 plugin 出现生命周期相关命中项 |
相关 skills:、、、、。
capacitor-app-upgrade-v8-to-v9capacitor-plugin-upgrade-v8-to-v9capacitor-app-upgradescapacitor-deep-linkingcapacitor-push-notifications