mcpField
C
capacitor-uiscene-migration

capacitor-uiscene-migration

github.com/cap-goDevelopment7354 installsUpdated 2w ago
Repository

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

  1. 01Classify UIScene migration state
  2. 02Audit lifecycle and URL-handling code
  3. 03Guide AppDelegate and SceneDelegate changes
  4. 04Check plugin and SDK lifecycle concerns
  5. 05Verify builds and app behavior

Use cases

  1. 01Preparing Capacitor 8.4 or earlier for Xcode 27
  2. 02Resolving partial cap migrate results
  3. 03Preserving deep links after scene adoption
  4. 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-migration

After installing, type/capacitor-uiscene-migrationin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

metadata.json
references/
audit-patterns.md
pbxproj-registration.md
plugin-audit.md
scene-delegate-template.md

Details

From the project README

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

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.

FileLoad when
references/audit-patterns.mdStep 4: grep commands and how to classify each hit
references/scene-delegate-template.mdCreating or merging SceneDelegate, Info.plist manifest, AppDelegate hook, custom subclass and lifecycle variants
references/pbxproj-registration.mdRegistering SceneDelegate.swift without Xcode, or verifying the CLI did
references/plugin-audit.mdThe 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.

Comments

Markdown supported