About capacitor-uiscene-migration
A skill for auditing and migrating Capacitor iOS projects from the AppDelegate lifecycle to UIScene, as required for Xcode 27. It covers migration state, custom lifecycle and URL handling, and build and runtime checks.
Key features
- 01Classify UIScene migration state
- 02Audit lifecycle and URL-handling code
- 03Guide AppDelegate and SceneDelegate changes
- 04Check plugin and SDK lifecycle concerns
- 05Verify builds and app behavior
Use cases
- 01Preparing Capacitor 8.4 or earlier for Xcode 27
- 02Resolving partial cap migrate results
- 03Preserving deep links after scene adoption
- 04Checking plugins for UIScene compatibility
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ cap-go/ capgo-skills --skill capacitor-uiscene-migrationAfter installing, type/capacitor-uiscene-migrationin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
metadata.jsonreferences/audit-patterns.mdpbxproj-registration.mdplugin-audit.mdscene-delegate-template.mdDetails
From the project READMECapacitor 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/ios8.0-8.4, or on 8.5 withoutUIApplicationSceneManifestinInfo.plist - Xcode 27 build or runtime warning "CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene lifecycle"
npx cap migrateprintedUIScene migration: project is in a partial state ... Skipping automated migration- After adding a scene manifest: custom URL scheme, universal link,
appUrlOpen, or AppDelegateapplicationDidBecomeActivecode stopped running - Build error on
tmpWindoworTmpViewControllerafter 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
UIApplicationSceneManifestexists, 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.*Notificationlifecycle notifications still fire. SceneDelegateProxy(CAPSceneDelegateProxy.swift) re-posts.capacitorOpenURL,.capacitorOpenUniversalLink(same payload) andCDVPluginHandleOpenURL, and mirrors the URL intoApplicationDelegateProxy.shared.lastURL. Cold-start URLs and user activities are held until the first.capacitorViewDidAppear, so plugins see them after registration andApp.getLaunchUrl()works.- New 8.5-only notifications:
.capacitorSceneWillConnect,.capacitorSceneOpenURL,.capacitorSceneOpenUniversalLink(object =UIScene, payload inuserInfo). Not posted on 8.4. - JS
pause/resumeare driven byUIScene.didEnterBackgroundNotification/willEnterForegroundNotification, filtered to the bridge's scene. - Removed in 8.5:
TmpViewController,CapacitorBridge.tmpWindow,tmpViewControllerAppeared. Any reference is a build error; present frombridge.viewController. - The 8.5 SceneDelegate creates the window and
CAPBridgeViewController()in code. A custom bridge subclass set inMain.storyboardmust be instantiated in the SceneDelegate instead. cap migrate(CLI 8.5+) classifies the project by three signals: manifest inInfo.plist,SceneDelegate.swifton disk,UISceneConfiguration(name:inAppDelegate.swift. 0/3 -> migrates; 3/3 -> skips as done; 1-2/3 -> warns "partial state" and does nothing. It never overwrites an existingSceneDelegate.swift. It warns (does not fix) onUIApplication.shared.applicationState,tmpWindow,TmpViewController, and customopen url:/continue userActivity:bodies.
Procedure
Audit, report, ask, then edit. Never edit before the user has seen the findings.
- Detect repo type.
ios/App/App.xcodeproj-> app.Package.swift/*.podspecwithCAPPluginsources and no app project -> plugin; go to step 9. - Check versions and package manager.
@capacitor/iosmust reach^8.5.0.ios/App/Podfile-> CocoaPods;ios/App/CapApp-SPM-> SPM. The SceneDelegate is identical for both. - Classify with the snapshot's three signals (eligible / migrated / partial).
- Audit with the commands in
references/audit-patterns.md, coveringios/Appand installed plugins innode_modules. Record file:line for each hit. - 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 insidenode_modules). - Ask at the real judgement calls:
- Custom body in
open url:/continue userActivity:: move it intoscene(_:openURLContexts:)/scene(_:continue:)next to the proxy call (show the body first). - Custom lifecycle code: move to
sceneDidBecomeActive/sceneWillResignActive/sceneDidEnterBackground/sceneWillEnterForeground, or toUIApplicationnotification 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.
- Custom body in
- 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, thennpx 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 perreferences/pbxproj-registration.md.
- Eligible and template-shaped:
- Sync and build:
npx cap sync ios, then build (see Verification). Re-check the three signals: all must be true. - 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
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
pauseandresumefire once each. - Custom scheme, warm (app running):
appUrlOpenfires. Usexcrun simctl openurl booted myscheme://test. - Custom scheme, cold (app killed, then link):
appUrlOpenfires andApp.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 (
registrationevent), and a tapped notification opens the app. - Any moved custom logic (analytics on foreground, SDK URL handlers) runs.
Error Handling
cap migratesays "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
UISceneDelegateClassNameinInfo.plistdoes not match the class. Use$(PRODUCT_MODULE_NAME).SceneDelegateand the template window setup. Cannot find 'SceneDelegate' in scopein AppDelegate -> file not in the App target; seereferences/pbxproj-registration.md.Value of type 'CapacitorBridge' has no member 'tmpWindow'-> delete the reference; present frombridge?.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
capacitorDidLoadmissing) -> instantiate the subclass inscene(_:willConnectTo:). pbxprojhand 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.