metadata.json
{
"version": "1.0.0",
"organization": "Capgo",
"date": "October 2026",
"abstract": "Audit-first migration of Capacitor iOS apps and plugins to the UIScene lifecycle (Capacitor 8.5, required by Xcode 27): SceneDelegate, Info.plist scene manifest, AppDelegate configurationForConnecting, pbxproj registration, moving custom URL and lifecycle code, plugin lifecycle audit, and device verification.",
"triggers": [
"capacitor 8.5",
"capacitor uiscene",
"add scenedelegate capacitor",
"adopt uiscene lifecycle",
"xcode 27 capacitor",
"CLIENT OF UIKIT REQUIRES UPDATE",
"UIApplicationSceneManifest",
"cap migrate partial state",
"appUrlOpen stopped working ios",
"tmpWindow removed"
],
"references": [
"https://capacitorjs.com/docs/updating/8-5",
"https://github.com/ionic-team/capacitor/blob/main/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift",
"https://github.com/ionic-team/capacitor/blob/main/cli/src/tasks/migrate-uiscene.ts",
"https://developer.apple.com/documentation/uikit/uiscenedelegate"
]
}
references/audit-patterns.md
# UIScene Audit Patterns
Run from the app root. Scope: `ios/App` (skip `Pods`, `build`, `DerivedData`, `.build`) and installed plugin sources in `node_modules`. Report every hit with file:line before editing anything.
```bash
EXCL="--exclude-dir=Pods --exclude-dir=build --exclude-dir=DerivedData --exclude-dir=.build"
```
## 1. Build blockers on 8.5
```bash
grep -rn $EXCL --include=*.swift --include=*.m --include=*.h -E '\btmpWindow\b|\bTmpViewController\b|tmpViewControllerAppeared' ios/App
```
Fix: delete. If the code presented UI from the temporary window, present from `bridge?.viewController` instead.
## 2. AppDelegate methods that stop running under scenes
```bash
grep -n -E 'func application\(.*open url:|continue userActivity:|applicationDidBecomeActive|applicationWillResignActive|applicationDidEnterBackground|applicationWillEnterForeground' ios/App/App/AppDelegate.swift
```
For each hit, read the body:
- Empty, comments only, or only `return ApplicationDelegateProxy.shared.application(...)` -> template-shaped, nothing to move. This is the same test `cap migrate` uses.
- Anything else -> custom logic. Ask, then move it:
| AppDelegate method | Scene destination |
|---|---|
| `application(_:open:options:)` | `scene(_:openURLContexts:)` (loop `URLContexts`, use `context.url`, `context.options.sourceApplication`) |
| `application(_:continue:restorationHandler:)` | `scene(_:continue:)` |
| `applicationDidBecomeActive` | `sceneDidBecomeActive(_:)` |
| `applicationWillResignActive` | `sceneWillResignActive(_:)` |
| `applicationDidEnterBackground` | `sceneDidEnterBackground(_:)` |
| `applicationWillEnterForeground` | `sceneWillEnterForeground(_:)` |
Cold launch: a URL or activity that launched the app arrives in `connectionOptions.urlContexts` / `connectionOptions.userActivities` inside `scene(_:willConnectTo:options:)`, not through `openURLContexts`. `SceneDelegateProxy` already handles this for Capacitor; custom logic that also needs the cold URL must read `connectionOptions` too.
## 3. Third-party SDK URL forwarding
```bash
grep -rn $EXCL --include=*.swift -E 'ApplicationDelegate\.shared|GIDSignIn|handleOpenURL|handle\(url|open url' ios/App/App
```
SDKs that were fed URLs from the AppDelegate need the same call in `scene(_:openURLContexts:)`. Check the SDK's docs for its scene API rather than guessing a signature.
## 4. Existing scene setup (partial state)
```bash
ls ios/App/App/SceneDelegate.swift 2>/dev/null
grep -n -A3 'UIApplicationSceneManifest' ios/App/App/Info.plist
grep -n 'UISceneConfiguration(name:\|configurationForConnecting' ios/App/App/AppDelegate.swift
grep -n 'SceneDelegate.swift' ios/App/App.xcodeproj/project.pbxproj
```
Existing SceneDelegate: list its methods. Required: window creation + `SceneDelegateProxy.shared` forwarders in `willConnectTo`, `openURLContexts`, `continue`. Merge missing ones; never replace the file.
## 5. Custom bridge view controller
```bash
grep -rn $EXCL --include=*.swift -E ':\s*CAPBridgeViewController' ios/App
grep -n 'customClass=' ios/App/App/Base.lproj/Main.storyboard
```
A subclass (often `MyViewController` with `capacitorDidLoad` registering local plugins) must be the `rootViewController` in the SceneDelegate.
## 6. Informational
```bash
grep -rn $EXCL --include=*.swift -E 'UIApplication\.shared\.applicationState|keyWindow|UIApplication\.shared\.windows|UIScreen\.main' ios/App
grep -rn --include=*.swift -E 'capacitorOpenURL|capacitorOpenUniversalLink|CDVPluginHandleOpenURL' node_modules/@capacitor* node_modules/@capgo 2>/dev/null
```
- `applicationState`: still works single-scene; scene-aware code reads `view.window?.windowScene?.activationState`.
- `keyWindow` / `windows` / `UIScreen.main`: work today, but they are the APIs Apple flags for resizable windows and foldables. Report, do not rewrite during this migration.
- Legacy URL notification observers: keep working, payload unchanged.
## 7. Plugins in node_modules
```bash
grep -rln --include=*.swift -E 'tmpWindow|TmpViewController|applicationDidBecomeActive|applicationDidEnterBackground|func application\(.*open url:' node_modules 2>/dev/null | grep '/ios/'
```
Report with plugin name and version (`node -p "require('<pkg>/package.json').version"`). Do not edit vendored code; upgrade the plugin or file an issue upstream. See `plugin-audit.md` for the verdicts.
references/pbxproj-registration.md
# Registering SceneDelegate.swift in project.pbxproj
A file on disk is not compiled until the App target knows about it. Symptom of a missing registration: `Cannot find 'SceneDelegate' in scope` in `AppDelegate.swift`, or a black screen because the class named in `Info.plist` does not exist at runtime.
## Preferred: let a tool do it
1. `npx cap migrate` (CLI 8.5+) registers it when the project is eligible (logs "Registering SceneDelegate.swift with the Xcode App target").
2. Xcode: right-click the `App` group -> Add Files to "App" -> select `SceneDelegate.swift`, tick the `App` target.
## Check first
```bash
grep -c 'SceneDelegate.swift' ios/App/App.xcodeproj/project.pbxproj
grep -n 'PBXFileSystemSynchronizedRootGroup' ios/App/App.xcodeproj/project.pbxproj
```
- Count 4 -> registered (file reference, build file, group child, Sources phase entry).
- If the App folder is a synchronized folder (`PBXFileSystemSynchronizedRootGroup`, Xcode 16+ projects), files inside it are compiled automatically; do not add explicit entries.
## Manual edit (only when Xcode is unavailable)
Four edits, two new 24-character hex IDs that do not already appear in the file (generate with `openssl rand -hex 12 | tr a-f A-F`). Shown with the IDs from the Capacitor 8.5 template; use your own.
```text
/* Begin PBXBuildFile section */
9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; };
/* Begin PBXFileReference section */
9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
/* the PBXGroup whose comment is "App" (the one listing AppDelegate.swift), children = ( */
9582B6822FE993A50072D4E8 /* SceneDelegate.swift */,
/* the App target's PBXSourcesBuildPhase, files = ( next to AppDelegate.swift in Sources */
9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */,
```
Rules:
- Group child and file reference use the file-reference ID; Sources phase uses the build-file ID.
- Use tabs, matching the surrounding lines.
- Add to the App target only, not to extension targets (widgets, notification service).
Validate:
```bash
plutil -lint ios/App/App.xcodeproj/project.pbxproj
xcodebuild -project ios/App/App.xcodeproj -list
```
If either fails, revert the file (`git checkout -- ios/App/App.xcodeproj/project.pbxproj`) after confirming with the user, and register through Xcode.
references/plugin-audit.md
# Plugin Audit for the UIScene Lifecycle
Plugins have no Info.plist or AppDelegate to patch. The question is whether the plugin's iOS code assumes the AppDelegate lifecycle, and whether one release can serve apps on 8.4 and 8.5+ (and Capacitor 9, where every app is scene-based).
First read the supported range: `peerDependencies["@capacitor/core"]`, the `capacitor-swift-pm` requirement in `Package.swift`, and the `Capacitor` dependency in the `.podspec`.
## Scan
```bash
grep -rn --include=*.swift --include=*.m -E 'tmpWindow|TmpViewController|applicationState|keyWindow|UIApplication\.shared\.windows|UIApplication\.shared\.delegate|applicationDidBecomeActive|applicationDidEnterBackground|applicationWillEnterForeground|applicationWillResignActive|capacitorOpenURL|capacitorOpenUniversalLink|CDVPluginHandleOpenURL|capacitorScene|SceneDelegateProxy|method_exchangeImplementations' ios
```
## Verdicts
| Pattern | Verdict |
|---|---|
| `tmpWindow`, `TmpViewController` | Must fix: removed in 8.5. Present from `bridge?.viewController` |
| Swizzling or casting `UIApplication.shared.delegate` to call/override `application(_:open:)`, `continue userActivity`, or the four lifecycle methods | Must fix: those methods no longer run in scene apps. Observe notifications instead |
| Observers of `UIApplication.didBecomeActiveNotification`, `willResignActive`, `didEnterBackground`, `willEnterForeground` | Fine: still posted |
| Observers of `.capacitorOpenURL`, `.capacitorOpenUniversalLink`, `CDVPluginHandleOpenURL` | Fine: re-posted on the scene path with the same payload |
| `ApplicationDelegateProxy.shared.lastURL` | Fine: populated from the scene path |
| `.capacitorScene*` notifications or `SceneDelegateProxy` | 8.5+ only. Does not compile against 8.4; never fires on 8.4. OK only if the plugin's minimum is 8.5 |
| `UIApplication.shared.applicationState` | Advisory: works single-scene; prefer `bridge?.webView?.window?.windowScene?.activationState` |
| `keyWindow?.rootViewController` for presenting | Advisory: prefer `bridge?.viewController` |
## One release for 8.4 and 8.5+
- Use the legacy notification names (posted on both).
- Do not reference removed APIs (`tmpWindow`, `TmpViewController`).
- Do not reference 8.5-only symbols unless the minimum is 8.5.
- Do lifecycle work through `UIApplication` notifications, not AppDelegate methods.
A plugin meeting all four needs no scene-specific release. For a Capacitor 9 release, the minimum is 9 anyway, so scene-only APIs are fine; see `capacitor-plugin-upgrade-v8-to-v9`.
## Output
Group findings as must fix / should fix / advisory / fine, with file:line. Edit only the plugin's own sources and only when the user asks. Verify in the example app with a scene-based iOS project (8.5+ template): pause/resume, URL open cold and warm, and any plugin feature that reacts to foreground/background.
references/scene-delegate-template.md
# Scene Delegate Template and Merge Recipes
All three pieces must exist together. Two out of three is the "partial" state.
## 1. `ios/App/App/SceneDelegate.swift` (Capacitor 8.5 template, SPM and CocoaPods)
```swift
import UIKit
import Capacitor
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 }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = CAPBridgeViewController()
window?.makeKeyAndVisible()
SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
SceneDelegateProxy.shared.scene(scene, continue: userActivity)
}
}
```
### Variant: custom bridge subclass
Replace `CAPBridgeViewController()` with the subclass, e.g. `MyViewController()`. If the subclass was only loaded from the storyboard and needs storyboard outlets, instantiate it from the storyboard instead:
```swift
window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
```
### Variant: moved custom URL logic
Keep the proxy call first so Capacitor plugins and `appUrlOpen` still see the URL, then run the app's own code:
```swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts)
for context in URLContexts {
// moved from AppDelegate.application(_:open:options:)
MyRouter.handle(context.url)
}
}
```
If that logic must also run on cold launch, read `connectionOptions.urlContexts` in `scene(_:willConnectTo:options:)` after the proxy call.
### Variant: moved lifecycle code
```swift
func sceneDidBecomeActive(_ scene: UIScene) { /* was applicationDidBecomeActive */ }
func sceneWillResignActive(_ scene: UIScene) { /* was applicationWillResignActive */ }
func sceneWillEnterForeground(_ scene: UIScene) { /* was applicationWillEnterForeground */ }
func sceneDidEnterBackground(_ scene: UIScene) { /* was applicationDidEnterBackground */ }
```
Alternative that does not depend on which delegate is active: observe `UIApplication.didBecomeActiveNotification` etc. from `didFinishLaunchingWithOptions`.
## 2. `Info.plist` manifest
Merge inside the top-level `<dict>`. If a manifest already exists, add only missing keys and keep `UISceneDelegateClassName` pointing at the class you actually use.
```xml
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
```
Validate after editing: `plutil -lint ios/App/App/Info.plist`.
## 3. `AppDelegate.swift` hook
Insert before the class's closing brace; do not move other methods:
```swift
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let config = UISceneConfiguration(name: "Default Configuration",
sessionRole: connectingSceneSession.role)
config.delegateClass = SceneDelegate.self
return config
}
```
The 8.5 template AppDelegate no longer has `application(_:open:options:)` or `application(_:continue:restorationHandler:)`. Removing forwarder-only copies is safe; they never run once the manifest exists.
Keep push callbacks (`didRegisterForRemoteNotificationsWithDeviceToken`, `didFailToRegisterForRemoteNotificationsWithError`) in the AppDelegate. They are app-level and unaffected.
For Capacitor 9 also change `@UIApplicationMain` to `@main` (Swift 6 rejects the old attribute). On 8.5 the template still uses `@UIApplicationMain`, and `@main` works there too.
SKILL.md
---
name: capacitor-uiscene-migration
description: Migrates a Capacitor iOS app or plugin from the AppDelegate lifecycle to the UIScene lifecycle introduced in Capacitor 8.5 and required by Xcode 27. Audits first (custom AppDelegate URL/lifecycle code, CAPBridgeViewController subclasses, existing SceneDelegate.swift, Info.plist UIApplicationSceneManifest, project.pbxproj registration, plugin lifecycle observers, tmpWindow/TmpViewController), reports, then merges surgically or hands template-shaped projects to `npx cap migrate`. Use for "Capacitor 8.5 upgrade", "add SceneDelegate", "adopt UIScene", "CLIENT OF UIKIT REQUIRES UPDATE", deep links or appUrlOpen stopped firing after 8.5, "cap migrate skipped partial state", or as the prerequisite step before Capacitor 9. Do not use for Android, full major upgrades (use capacitor-app-upgrade-v8-to-v9), or new deep-link setup (use capacitor-deep-linking).
allowed-tools:
- Bash(node -e *)
- Bash(find *)
---
# Capacitor UIScene Migration (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
## 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
## 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'))"`
## 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.
## 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.
## 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.
## 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.
## References
Only load a reference when its topic is in play.
| File | Load 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`.