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.