diff --git a/README.md b/README.md index 905e4e4..bf355b2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A catalog of agent skills for working with Capacitor. ## Overview -This repository contains agent skills focused on the Capacitor development lifecycle. Currently, it includes skills specific to plugins: generating first-pass net-new plugins, migrating existing Cordova plugins to Capacitor, and configuring OutSystems Developer Cloud (ODC) build actions for plugins. +This repository contains agent skills focused on the Capacitor development lifecycle: generating first-pass net-new plugins, migrating existing Cordova plugins to Capacitor, configuring OutSystems Developer Cloud (ODC) build actions for plugins, and migrating Capacitor apps to the 8.5 UIScene lifecycle. The skills are written to be agent-agnostic. They package domain knowledge, best practices, and repeatable workflows as plain instructions and reference material, so any agent that supports the [skills](https://skills.sh) format can use them. @@ -28,6 +28,12 @@ Generates OutSystems Developer Cloud (ODC) build action JSON files that configur **Use when:** Creating a build action for an ODC plugin, generating a `buildAction.json` file, setting up Gradle or plist build actions, or configuring `AndroidManifest` for an ODC build. Note that build actions only apply to ODC — they have no effect in standalone Capacitor apps. +### capacitor-uiscene-migrator + +Assists with the Capacitor 8.4 → 8.5 UIScene migration for iOS, covering what `npx cap migrate` skips or only warns about: partially migrated projects, hand-rolled scene delegates, and custom `application(_:open:)` bodies. It audits before editing, asks at judgement points, merges into existing files instead of overwriting them, and hands template-shaped projects to the CLI migrator. + +**Use when:** Migrating a Capacitor 8.x iOS app to the UIScene lifecycle, finishing a migration the CLI skipped as partial, or auditing a Capacitor plugin for UIScene compatibility. Scope is 8.4 → 8.5 only. + ## Installation Install skills directly from this repository using [skills.sh](https://skills.sh): @@ -40,6 +46,7 @@ npx skills add ionic-team/capacitor-skills npx skills add ionic-team/capacitor-skills/capacitor-plugin-generator npx skills add ionic-team/capacitor-skills/cordova-plugin-migrator npx skills add ionic-team/capacitor-skills/build-actions-generator +npx skills add ionic-team/capacitor-skills/capacitor-uiscene-migrator ``` ## Local Development @@ -71,6 +78,7 @@ Once installed, the skills become available to your agent and activate based on "Migrate this Cordova plugin to Capacitor" "Assess the migration feasibility of cordova-plugin-camera" "Create an ODC build action for my Capacitor plugin" +"Migrate my Capacitor app to UIScene" ``` The relevant skill activates automatically when your request matches what it handles. diff --git a/skills/capacitor-uiscene-migrator/SKILL.md b/skills/capacitor-uiscene-migrator/SKILL.md new file mode 100644 index 0000000..eb6017e --- /dev/null +++ b/skills/capacitor-uiscene-migrator/SKILL.md @@ -0,0 +1,297 @@ +--- +name: capacitor-uiscene-migrator +description: >- + Assists Capacitor developers migrating an iOS app or plugin from 8.4 to + the 8.5 UIScene lifecycle, covering what `npx cap migrate` skips or only + warns about: partially migrated projects, hand-rolled scene delegates, + and custom application(_:open:) bodies that must move by hand. Audits + first, reports findings, asks the developer at judgement points, merges + surgically instead of overwriting, and hands off to `npx cap migrate` + when the project matches the template shape. Branches for Capacitor + plugin repos, auditing Swift for lifecycle assumptions without touching + app-level files. Use when the user says "migrate my Capacitor app to + UIScene", "add SceneDelegate support", "Capacitor 8.5 migration", + "update to Capacitor 8.5", "adopt the scene lifecycle", "Xcode 27 + Capacitor build failing", or mentions the "CLIENT OF UIKIT REQUIRES + UPDATE" warning. Do not use for Cordova-to-Capacitor migration (use + cordova-plugin-migrator), generating new plugins + (use capacitor-plugin-generator), or Capacitor 9 migrations. +metadata: + author: ionic + source: https://github.com/ionic-team/capacitor-skills +--- + +# Capacitor UIScene Migrator + +Guides a Capacitor 8.4 → 8.5 iOS migration to the UIScene lifecycle. The +CLI migrator (`npx cap migrate`) handles projects that still match the +Capacitor templates; this skill exists for everything else. It audits +before editing, asks the developer where judgement is required, merges +into existing files rather than replacing them, and delegates the +mechanical work to the CLI whenever the project shape allows. + +The canonical migration reference is the +[8.4 → 8.5 migration guide](https://capacitorjs.com/docs/updating/8-5). +Link it whenever a step is left for the developer to do manually. + +## When to Use This Skill + +- Migrating a Capacitor 8.x iOS app to the UIScene lifecycle +- A project where `npx cap migrate` reported a partial state and skipped +- An app with a hand-rolled `SceneDelegate.swift` or a customized + `AppDelegate.swift` (deep-link routing, third-party SDK forwarding) +- Auditing a Capacitor plugin repo for UIScene compatibility +- Diagnosing the Xcode "CLIENT OF UIKIT REQUIRES UPDATE: This process + does not adopt UIScene lifecycle" warning in a Capacitor app + +## When NOT to Use This Skill + +- Cordova-to-Capacitor plugin migration → `cordova-plugin-migrator` +- Generating a new Capacitor plugin → `capacitor-plugin-generator` +- Capacitor 9 or later migrations (this skill is 8.4 → 8.5 only) +- Android lifecycle work (UIScene is iOS only) +- General Capacitor debugging unrelated to the scene lifecycle + +## Prerequisites + +- A Capacitor 8.x project (app or plugin) with an `ios/` platform or + iOS sources +- `@capacitor/cli` 8.5+ available for the `npx cap migrate` handoff +- Xcode installed if the developer wants build verification + +## What Changed in 8.5 (facts the audit relies on) + +These determine what breaks and what does not. Do not soften them. + +- Scene adoption is opt-in. An app with no `UIApplicationSceneManifest` + keeps the AppDelegate path, which still works on the 8.5 core. iOS + posts `UIScene.*` notifications even for legacy apps (compatibility + scene), so the bridge's JS `resume`/`pause` events fire in both modes. +- Once the scene manifest exists, iOS stops calling + `application(_:open:options:)`, `application(_:continue:)`, and the + four foreground/background AppDelegate methods + (`applicationDidBecomeActive`, `applicationWillResignActive`, + `applicationDidEnterBackground`, `applicationWillEnterForeground`). + Custom code in any of those silently stops running. The + `UIApplication` notifications still fire; `didFinishLaunching`, + `applicationWillTerminate`, push token registration, and the + remote-notification callbacks stay on the AppDelegate. +- `SceneDelegateProxy` re-posts the legacy `.capacitorOpenURL`, + `.capacitorOpenUniversalLink`, and `CDVPluginHandleOpenURL` + notifications with the same payload shape, so existing observers keep + working. Cold-start URLs are delivered after the bridge view appears, + so both `appUrlOpen` and `getLaunchUrl()` work on cold launch. +- New `.capacitorSceneWillConnect`, `.capacitorSceneOpenURL`, and + `.capacitorSceneOpenUniversalLink` notifications carry the `UIScene` + as the object. They are only posted on 8.5+; plugins that also support + 8.4 must keep using the legacy names. +- `TmpViewController` and `CapacitorBridge.tmpWindow` were removed. Any + reference is a build error on 8.5. +- The 8.5 templates create the window in code in + `scene(_:willConnectTo:)`; `Main.storyboard` no longer provides the + root view controller. Custom `CAPBridgeViewController` subclasses are + instantiated in the SceneDelegate, not set in the storyboard. + +## Agent Behavior + +- Audit first, report second, edit last. Never modify a file before the + developer has seen the findings and confirmed. +- Never overwrite an existing `SceneDelegate.swift`, `AppDelegate.swift`, + or `Info.plist` structure. Merge surgically per + [references/surgical-merges.md](references/surgical-merges.md). +- Prefer the CLI. If the project classifies as eligible (Phase 3), run + `npx cap migrate` instead of editing files by hand. +- Ask, do not assume, at the decision points in Phase 6. Use + `AskUserQuestion` where available; otherwise ask in plain text and + wait. +- Do not commit, stage, or push. Leave version control to the developer. +- On a plugin repo, never touch `Info.plist`, `AppDelegate.swift`, or + project files. The plugin branch is audit and advice only. + +## Procedures + +### Phase 1: Detect Repo Type + +Decide app vs. plugin before anything else. + +- **App**: has `ios/App/App.xcodeproj` (or `capacitor.config.*` with an + `ios/` platform directory). +- **Plugin**: has a `Package.swift` or `.podspec` depending on + Capacitor, `CAPPlugin`/`CapacitorPlugin` subclasses in `ios/Sources` + or `ios/Plugin`, and no `App/App.xcodeproj`. + +Plugin repo → skip to Phase 10 +([references/plugin-repo-audit.md](references/plugin-repo-audit.md)). + +### Phase 2: Detect Package Manager and Versions + +- Pods vs. SPM: `ios/App/Podfile` → CocoaPods; `Package.swift` or an SPM + reference inside the Xcode project → SPM. This affects how `npx cap + sync ios` behaves, not the SceneDelegate content: the 8.5 templates + ship one SceneDelegate for both. +- Check `@capacitor/ios` version in `package.json`. If below 8.5, the + dependency update is part of the migration; the CLI migrator handles + it, or update manually per the guide. + +### Phase 3: Classify the Project State + +Read the same three signals the CLI migrator uses: + +1. `Info.plist` contains `UIApplicationSceneManifest` +2. `SceneDelegate.swift` exists on disk in the app target directory + (pbxproj registration is a separate concern, handled in Phase 7) +3. `AppDelegate.swift` contains `UISceneConfiguration(name:` + +| Signals present | State | Route | +|---|---|---| +| 0 of 3 | eligible | Phases 4-6, then Phase 7a: hand off to `npx cap migrate` | +| 3 of 3 | already migrated | Audit only (Phase 4), then verify (Phase 9) | +| 1-2 of 3 | partial | Phases 4-6, then Phase 7b: surgical merges | + +Every route audits before anything runs or changes; the routes differ +only in who applies the changes. + +The CLI warns and skips on partial states by design. Partial is exactly +where this skill does its own editing. + +### Phase 4: Audit the Codebase + +Run the scans in +[references/audit-patterns.md](references/audit-patterns.md) across the +app's iOS sources and installed plugins (`node_modules/@capacitor*`, +plus any local plugin paths). Collect findings for: + +- `UIApplication.shared.applicationState` usage +- Custom `application(_:open:)` / `application(_:continue:)` bodies + beyond the `ApplicationDelegateProxy` forwarder +- Custom code in AppDelegate lifecycle methods +- Existing `SceneDelegate.swift` implementations and what they contain +- References to `tmpWindow` / `TmpViewController` (build errors on 8.5) +- Existing or partial `UIApplicationSceneManifest` entries +- `.capacitorOpenURL` / `.capacitorOpenUniversalLink` observers + (informational: they keep working) + +### Phase 5: Present Findings and Confirm + +Report every finding with file and line before touching anything. +Group as: blocks the build (tmpWindow/TmpViewController), needs a +decision (custom delegate bodies, existing SceneDelegate), informational +(observers, applicationState in plugins the developer does not own). +Ask the developer to confirm proceeding. + +### Phase 6: Decision Points + +Ask, at minimum: + +1. **Legacy URL handlers**: keep or remove + `application(_:open:options:)` / `application(_:continue:)` in + `AppDelegate.swift`? They become dead code under scenes. Keeping them + is harmless; removing them is cleaner. If the body contains custom + logic, it must move to the SceneDelegate either way. On the eligible + route, apply the answer after the CLI has run. +2. **Custom `application(_:open:)` body**: migrate it manually (the + developer moves the logic) or have the skill move it into + `scene(_:openURLContexts:)` alongside the proxy forwarder? Show the + body before asking. Skip this question when the body is + forwarder-only per the audit test. +3. **Existing SceneDelegate**: confirm each proposed insertion + (missing forwarders, window setup) as a diff before applying. + +### Phase 7a: Eligible → CLI Handoff + +Verify the resolved CLI first: run `npm install` if `node_modules` is +missing, then `npx cap --version`; the UIScene migrator needs 8.5 or +newer. Then run `npx cap migrate` and interpret its output. It writes +`SceneDelegate.swift`, patches `Info.plist` and `AppDelegate.swift`, +registers the file in `project.pbxproj`, warns on the scan patterns from +Phase 4, and links the migration guide. Confirm each step's log line; +if the CLI skipped a step (file existed, partial state raced in), +fall through to Phase 7b for that step only. + +### Phase 7b: Partial or Hand-Rolled → Surgical Merges + +Apply only the missing pieces, per +[references/surgical-merges.md](references/surgical-merges.md): + +- Missing manifest → merge `UIApplicationSceneManifest` into + `Info.plist`, preserving existing keys +- Missing `configurationForConnecting` → insert into the existing + `AppDelegate.swift` before the class's closing brace, displacing + nothing +- Missing or incomplete `SceneDelegate.swift` → create from + [references/scene-delegate-template.md](references/scene-delegate-template.md), + or add the missing `SceneDelegateProxy.shared` forwarders to the + existing one, preserving all custom logic +- New file → register in `project.pbxproj` (Xcode does this when the + file is added through the IDE; otherwise follow the reference) + +### Phase 8: Sync and Build + +Run `npx cap sync ios`. If Xcode is available, build the app target and +surface any errors (most commonly leftover `tmpWindow` references). + +### Phase 9: Verification Checklist + +Walk the developer through, linking the guide for detail: + +- App launches to the web view +- Background and foreground the app: JS `resume`/`pause` fire (they are + scoped to the app's scene now) +- Custom URL scheme: cold launch and warm open both deliver the URL + (`appUrlOpen` listener and `App.getLaunchUrl()`) +- Universal link, if the app uses them (requires an associated domain) + +### Phase 10: Plugin Repo Branch + +No app-level files to patch. Audit the plugin's Swift per +[references/plugin-repo-audit.md](references/plugin-repo-audit.md) and +report: lifecycle assumptions that break under scenes, APIs removed in +8.5, and the compatibility rules for supporting 8.4 and 8.5 with one +plugin version. Apply code changes only if the developer asks, and only +in the plugin's own sources. + +## Best Practices + +### DO + +- Show diffs before applying them +- Keep every finding tied to a file and line +- Re-run the Phase 3 classification after edits to confirm the project + reads as fully migrated +- Tell plugin authors to keep the legacy notification names while they + support 8.4 + +### DON'T + +- Overwrite user code, ever +- Rewrite third-party plugin code under `node_modules` (report it; + the fix belongs upstream) +- Duplicate the CLI's work by hand-editing an eligible project +- Promise universal-link behavior without an associated domain to test + +## Error Handling + +- `npx cap migrate` warns "partial state" → expected; this skill's + Phase 7b exists for that. Do not reset the project without asking. +- `project.pbxproj` edits fail or the project no longer opens → revert + the pbxproj change and register the file through Xcode instead. +- Build fails on `tmpWindow` / `TmpViewController` → the references + must be deleted; there is no 8.5 replacement (the bridge's + `viewController` is the presentation anchor). +- `SceneDelegate` exists but the app shows a black screen → the + delegate neither creates a window nor lets a storyboard do it; add + the window setup from the template. + +## Related Skills + +- `cordova-plugin-migrator`: Cordova plugin to Capacitor migration +- `capacitor-plugin-generator`: new Capacitor plugin scaffolds + +## References + +| File | Purpose | +|---|---| +| [references/scene-delegate-template.md](references/scene-delegate-template.md) | The 8.5 SceneDelegate template and custom-subclass variant | +| [references/audit-patterns.md](references/audit-patterns.md) | Exact scan patterns with commands | +| [references/surgical-merges.md](references/surgical-merges.md) | Merge recipes for partial and hand-rolled projects | +| [references/plugin-repo-audit.md](references/plugin-repo-audit.md) | Plugin-author branch: audit and compatibility rules | diff --git a/skills/capacitor-uiscene-migrator/references/audit-patterns.md b/skills/capacitor-uiscene-migrator/references/audit-patterns.md new file mode 100644 index 0000000..29cf232 --- /dev/null +++ b/skills/capacitor-uiscene-migrator/references/audit-patterns.md @@ -0,0 +1,48 @@ +# Audit Patterns + +Run these before proposing any edit. Scan the app's `ios/` tree +(excluding `Pods/`, `build/`, `DerivedData/`, `.build/`) and installed +plugin sources under `node_modules/@capacitor*` plus any local plugin +paths from `package.json`. + +The CLI migrator's own scan warns on `tmpWindow`, `TmpViewController`, +`UIApplication.shared.applicationState`, and custom `open:`/`continue:` +bodies; the remaining checks are this skill's own. + +## Blocks the 8.5 build + +| Pattern | Meaning | Remedy | +|---|---|---| +| `\btmpWindow\b` | Removed from `CapacitorBridge` in 8.5 | Delete. If the code presented UI from it, present from `bridge.viewController`; other uses have no 8.5 replacement, so ask what the code intended | +| `\bTmpViewController\b` | Removed in 8.5 | Delete the reference | + +```bash +rg -n --type swift -e '\btmpWindow\b' -e '\bTmpViewController\b' ios \ + -g '!**/Pods/**' -g '!**/build/**' -g '!**/DerivedData/**' +``` + +## Needs a decision (Phase 6) + +| Pattern | Meaning | +|---|---| +| `func application\([^)]*\bopen url:` with a body beyond `return ApplicationDelegateProxy.shared.application(...)` | Custom deep-link logic that stops running under scenes; must move to `scene(_:openURLContexts:)` | +| `func application\([^)]*\bcontinue userActivity:` with a custom body | Same, for universal links; moves to `scene(_:continue:)` | +| `applicationDidBecomeActive\|applicationWillResignActive\|applicationDidEnterBackground\|applicationWillEnterForeground` with non-empty bodies in `AppDelegate.swift` (comment-only counts as empty) | AppDelegate lifecycle methods stop being called under scenes; move code to the SceneDelegate equivalents or observe the `UIApplication` notifications (which still fire) | +| Existing `SceneDelegate.swift` | Inventory its methods; merge, never replace | +| Existing `UIApplicationSceneManifest` in `Info.plist` | Compare against the template manifest; merge missing keys only | +| Custom class on the storyboard's bridge view controller (`customClass=` in `Main.storyboard` on the CAPBridgeViewController scene) | Must be carried into the SceneDelegate's `rootViewController` line. If the plist references a storyboard that is missing from disk, note it and skip this check | + +A forwarder-only body is not custom. This is the same test the CLI +uses: strip the `ApplicationDelegateProxy` call and comments; if +nothing meaningful remains, the method is template-shaped. + +## Informational (report, do not edit) + +| Pattern | Meaning | +|---|---| +| `UIApplication.shared.applicationState` | Still works in a single-scene app. Note it; scene-scoped code should prefer the window scene's `activationState` | +| `NotificationCenter` observers for `capacitorOpenURL` / `capacitorOpenUniversalLink` / `CDVPluginHandleOpenURL` | Keep working on 8.5; the scene path re-posts them with the same payload | +| `.capacitorSceneOpenURL` / `.capacitorSceneWillConnect` / `.capacitorSceneOpenUniversalLink` usage in a plugin | Only posted on 8.5+; flag if the plugin also targets 8.4 | + +Findings inside `node_modules` belong to the plugin's upstream. Report +them with the plugin name and version; never edit vendored code. diff --git a/skills/capacitor-uiscene-migrator/references/plugin-repo-audit.md b/skills/capacitor-uiscene-migrator/references/plugin-repo-audit.md new file mode 100644 index 0000000..d68c423 --- /dev/null +++ b/skills/capacitor-uiscene-migrator/references/plugin-repo-audit.md @@ -0,0 +1,57 @@ +# Plugin Repo Audit + +A Capacitor plugin has no `Info.plist`, `AppDelegate`, or scene manifest +to patch. The migration question for a plugin is narrower: does its +Swift make assumptions the scene lifecycle breaks, and can one release +support both 8.4 and 8.5? + +## Detection + +Treat the repo as a plugin when it has a `Package.swift` or `.podspec` +depending on Capacitor, `CAPPlugin` subclasses under `ios/Sources` or +`ios/Plugin`, and no `App/App.xcodeproj`. An `example-app/` directory +containing an app project does not make it an app repo; audit the +plugin sources, and treat the example app as an app migration only if +the developer asks. + +Read the supported Capacitor range first (`peerDependencies` in +`package.json`, the Capacitor dependency in `Package.swift` or the +podspec); the one-plugin-version rule below depends on it. + +## What to scan for + +| Pattern | Verdict | +|---|---| +| `tmpWindow`, `TmpViewController` | Build error on 8.5. Must be removed. If the code presented UI from it, present from `bridge.viewController`; other uses have no 8.5 replacement, so ask what the code intended | +| Observers for `capacitorOpenURL`, `capacitorOpenUniversalLink`, `CDVPluginHandleOpenURL` | Safe. The 8.5 scene path re-posts all three unchanged: the Capacitor names carry a dictionary (`url`, plus `options` for scheme opens); `CDVPluginHandleOpenURL` carries the `URL` itself | +| Observers for `UIApplication` lifecycle notifications (`didBecomeActive`, `willResignActive`, `didEnterBackground`, `willEnterForeground`) | Safe. These still fire in scene-based apps | +| Overrides or expectations of AppDelegate lifecycle *methods* | Breaks under scenes; iOS calls the scene delegate instead. Move to notification observers | +| `UIApplication.shared.applicationState` | Works in single-window apps. For scene-scoped checks, prefer the view's `window?.windowScene?.activationState` (the pattern Capacitor core uses) | +| `ApplicationDelegateProxy.shared.lastURL` reads | Safe. The scene path mirrors the launch URL into it | +| `.capacitorScene*` notification usage | 8.5-only. A plugin that also supports 8.4 must not depend on these; keep the legacy names | +| Presenting from `UIApplication.shared.keyWindow?.rootViewController` | Works single-window; prefer `bridge.viewController` for correctness and future multi-window | + +## The one-plugin-version rule + +A plugin that must build and run across Capacitor 8.4 and 8.5 (apps on +either AppDelegate or UIScene): + +- Consume the legacy notification names; they are posted on both +- Do not reference removed APIs (`tmpWindow`, `TmpViewController`) +- Do not reference 8.5-only APIs (`SceneDelegateProxy`, the + `.capacitorScene*` constants); they do not compile against 8.4. + Observing the equivalent raw-string notification names compiles + everywhere but never fires on 8.4 +- Keep lifecycle handling on `UIApplication` notifications, not + AppDelegate methods + +A plugin meeting all four needs no 8.5-specific release. + +## Output + +Report findings grouped as: must fix (build errors), should fix +(breaks under scenes), advisory (works but a better pattern exists), +fine as-is. Apply changes only in the plugin's +own sources and only when the developer confirms. Link the +[migration guide](https://capacitorjs.com/docs/updating/8-5) for the +app-side context. diff --git a/skills/capacitor-uiscene-migrator/references/scene-delegate-template.md b/skills/capacitor-uiscene-migrator/references/scene-delegate-template.md new file mode 100644 index 0000000..ebc4190 --- /dev/null +++ b/skills/capacitor-uiscene-migrator/references/scene-delegate-template.md @@ -0,0 +1,99 @@ +# SceneDelegate Template (Capacitor 8.5) + +The 8.5 templates ship one `SceneDelegate.swift` for both CocoaPods and +SPM projects. Source of truth: `ios-pods-template/App/App/SceneDelegate.swift` +and `ios-spm-template/App/App/SceneDelegate.swift` in the Capacitor repo +(byte-identical). + +```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) { + SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } +} +``` + +Every `SceneDelegateProxy.shared` forwarder is load-bearing: + +- `willConnectTo` delivers cold-start URLs and universal links (buffered + by the core until plugins load, so `appUrlOpen` fires on cold launch) +- `openURLContexts` delivers warm custom-scheme opens +- `continue` delivers warm universal links + +## Custom view controller subclass + +The delegate creates the root view controller in code; `Main.storyboard` +no longer provides it. A custom `CAPBridgeViewController` subclass is +instantiated here, not set in the storyboard: + +```swift +window?.rootViewController = MyViewController() +``` + +If an existing app set the subclass in `Main.storyboard` (the pre-8.5 +customization pattern), moving to this template silently drops the +subclass unless the developer moves it into this line. Always check the +storyboard's custom class during the audit and carry it over. + +## AppDelegate scene hook + +The matching `AppDelegate.swift` addition: + +```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 +} +``` + +## Info.plist scene manifest + +```xml +UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + +``` + +This matches the shipped templates, including `UISceneStoryboardFile`. +Keep that key when the app has a `Main.storyboard`; skip it when it +does not (surgical-merges recipe 2). Either way, the delegate's window +setup is what puts content on screen. diff --git a/skills/capacitor-uiscene-migrator/references/surgical-merges.md b/skills/capacitor-uiscene-migrator/references/surgical-merges.md new file mode 100644 index 0000000..a6c8830 --- /dev/null +++ b/skills/capacitor-uiscene-migrator/references/surgical-merges.md @@ -0,0 +1,80 @@ +# Surgical Merges + +Recipes for the partial and hand-rolled states the CLI refuses. Each +recipe adds only what is missing and preserves everything the developer +wrote. Show the diff and get confirmation before applying any of them. + +## 1. Insert `configurationForConnecting` into an existing AppDelegate + +Insert the scene hook (see scene-delegate-template.md) immediately +before the closing brace of the `AppDelegate` class, after the last +existing method. Do not reorder, reformat, or remove anything else. + +Idempotency check first: if the file already contains +`UISceneConfiguration(name:`, the hook exists; skip. + +If the class ends in nested types or extensions, anchor on the brace +that closes `class AppDelegate`, not the last brace in the file. + +## 2. Merge into an existing `UIApplicationSceneManifest` + +Never replace the dictionary. Add only missing keys: + +- `UIApplicationSupportsMultipleScenes` absent → add as `false`. Present + as `true` → leave it, and warn: Capacitor 8.5 supports single-window + only; multi-scene behavior is untested. +- `UISceneConfigurations` / `UIWindowSceneSessionRoleApplication` + absent → add the template's single configuration. +- A configuration entry exists but lacks `UISceneDelegateClassName` → + add `$(PRODUCT_MODULE_NAME).SceneDelegate`. If it names a different + delegate class, that class is the merge target for recipe 3; do not + repoint the manifest. +- For template parity, also add `UISceneStoryboardFile` = `Main` when + the app has a `Main.storyboard` on disk; skip the key when it does + not. +- Preserve the existing `UISceneConfigurationName`. If it is not + `"Default Configuration"`, use the existing name in the + `configurationForConnecting` hook so the two agree. + +Edit the plist as XML respecting its structure (or PlistBuddy); no +regex string substitution on nested dicts. + +## 3. Add missing forwarders to an existing SceneDelegate + +Inventory which of the three scene methods exist, then: + +- Method missing entirely → add it containing only the + `SceneDelegateProxy.shared` forwarder. +- Method exists without the forwarder → add the forwarder call, + preserving the developer's code. In `willConnectTo`, place the + forwarder after any window setup; in `openURLContexts` and + `continue`, placement relative to custom code is the developer's + call. Ask if the custom logic consumes the same URL. +- Delegate creates no window and no storyboard is wired to the scene + config → add the window setup from the template (this is the + black-screen case). + +## 4. Register a new file in `project.pbxproj` + +Preferred: the developer adds `SceneDelegate.swift` to the App target +in Xcode, which writes all four entries correctly. If editing directly, +a new file needs a `PBXBuildFile` entry, a `PBXFileReference`, a child +entry in the App `PBXGroup`, and a line in the target's Sources build +phase, each with a unique 24-hex-character ID. Validate afterwards by +opening the project or running a build; a malformed pbxproj fails +loudly. If it breaks, revert this file only and use Xcode. + +## 5. Moving a custom `application(_:open:)` body + +When the developer opts for assisted migration (Phase 6, decision 2): + +1. Copy the custom logic into `scene(_:openURLContexts:)`, adapting the + signature: each `UIOpenURLContext` provides `.url` and `.options`. +2. Keep the `SceneDelegateProxy.shared` forwarder in place; the custom + logic runs alongside it, same as it ran alongside the + `ApplicationDelegateProxy` forwarder before. +3. Leave the old AppDelegate method in place until the developer's + keep-or-remove decision from Phase 6, decision 1; under scenes it no + longer runs either way. +4. `application(_:continue:)` bodies move to `scene(_:continue:)` the + same way; the `NSUserActivity` parameter carries over unchanged.