Every Capacitor major release since 6 has moved the native projects a little
further away from the CocoaPods-and-old-Gradle world most Ionic apps were
born in. Capacitor 7 made Swift Package Manager (SPM) a supported option on
iOS; Capacitor 8 makes it the default path, raises the Android toolchain
(AGP 8.x / Gradle 8.x+, compile and target SDK 36, JDK 21) and drops a pile
of long-deprecated APIs. If your app still builds with CocoaPods and an
android/ folder that has not been touched since Ionic 6, this is the
upgrade that unblocks the next two years of OS releases.
This tutorial is the sequence we run on client apps: audit, branch, iOS to SPM, Android toolchain, plugin triage, verify. Budget a day for a small app and a week for one with custom native code.
0. Audit before you touch anything
Run these first and paste the output into the migration ticket:
npx cap doctor
npm ls @capacitor/core @capacitor/ios @capacitor/android
npm outdated | grep -E 'capacitor|ionic'
cap doctor tells you the installed Capacitor versions per platform and
whether the native projects are in sync. The dependency list matters more:
every Capacitor plugin you depend on must ship a version built for the
new major. Official @capacitor/* plugins track the majors within days.
Community plugins are the risk — check each repo for a release tagged for
the new major, and note the ones that have gone quiet. A plugin that has had
no commits in eighteen months is a decision, not a dependency.
Also confirm your CI images. SPM builds and AGP 8.x need Xcode 16+ and JDK 21; a self-hosted runner pinned to JDK 17 will fail the first Android build after the upgrade.
1. Branch, and upgrade the JavaScript side first
git checkout -b chore/capacitor-8
npm i @capacitor/core@8 @capacitor/cli@8
npm i @capacitor/ios@8 @capacitor/android@8
npm i @capacitor/app@8 @capacitor/haptics@8 @capacitor/keyboard@8 # etc.
npx cap sync
Do not run the platform migration yet. Get npm run build green and the app
running in the browser (ionic serve) with the new core. Type errors here
are cheap; the same errors discovered after the native projects have been
rewritten are not.
2. iOS: move to Swift Package Manager
The CLI ships a migration command that rewrites the Xcode project to consume Capacitor and your plugins as Swift packages instead of Pods:
npx cap migrate # runs the guided migration
# or, iOS only:
npx cap update ios
What it does, and what you should verify by hand afterwards:
- Creates
ios/App/CapApp-SPM— a local Swift package that lists Capacitor and each plugin as a dependency. OpenCapApp-SPM/Package.swiftand check that every plugin in yourpackage.jsonappears. Plugins that have not published SPM support will be missing; that is your triage list for step 4. - Removes the CocoaPods integration. Delete the leftovers once the
build is green:
From here you opencd ios/App && rm -rf Pods Podfile Podfile.lock App.xcworkspaceApp.xcodeproj, not the workspace. Update any CI or fastlane step that still passes-workspace App.xcworkspace— this is the single most common breakage. - Bumps the deployment target. Capacitor 8 requires iOS 15+. If your Podfile pinned 13 or 14, check your analytics before shrugging: anything below ~0.3% of sessions is normally fine to drop, but it is a product decision, not a build decision.
Then build:
npx cap sync ios
npx cap open ios
If Xcode complains it cannot resolve packages, reset the cache with
File → Packages → Reset Package Caches, and make sure the checked-in
Package.resolved is committed — pinning it is what makes CI builds
reproducible.
Keeping a native pod you still need
If you have a native SDK that is only distributed as a pod (some analytics
and payment SDKs still are), you can keep CocoaPods alongside SPM for that
one dependency: leave a minimal Podfile targeting App with just that pod,
keep using the workspace, and let Capacitor itself come from SPM. It works,
but treat it as temporary and re-check the vendor's SPM support each quarter.
3. Android: toolchain, not code
Most of the Android work is configuration:
// android/build.gradle
classpath 'com.android.tools.build:gradle:8.7.2'
// android/variables.gradle
ext {
minSdkVersion = 23
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.9.2'
// ...
}
# android/gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
Three things bite here:
- JDK 21. Set it in Android Studio (Settings → Build Tools → Gradle) and
in CI (
actions/setup-javawithjava-version: 21). - Namespace, not
package. AGP 8 requiresnamespaceinandroid/app/build.gradleand forbidspackageinAndroidManifest.xml. Community plugins that still carrypackagewill fail the build — that is the second half of your triage list. - Target SDK 36 pulls in Android 16 behaviour: edge-to-edge is enforced and predictive back is on by default. We covered the layout side of that in edge-to-edge and safe areas; do that work in the same branch, because the upgrade is what exposes it.
4. Plugin triage
For each plugin the migration flagged, you have four options, in order of preference:
- Upgrade to a release that supports the new major.
- Replace it with an official
@capacitor/*equivalent — several community plugins (filesystem, preferences, geolocation wrappers) have official counterparts now. - Fork and patch. For an SPM-less plugin this is often a twenty-line
change: add a
Package.swift, move sources toSources/<PluginName>/, and point yourCapApp-SPM/Package.swiftat the fork with.package(path:)or a Git URL. For an Androidpackageattribute, move it tonamespace. - Absorb it. If the plugin wraps thirty lines of native code, write your own — see writing your own Capacitor plugin. One fewer unmaintained dependency is worth the afternoon.
5. Verify on device, not just in the simulator
The upgrade changes how the app is linked, so re-test the things that depend on native wiring:
- cold start and splash screen dismissal,
- push notification registration and a delivered notification tapped from cold start,
- deep links into a route that requires auth,
- camera, filesystem and any BLE/NFC hardware paths,
- an App Store Connect / Play Console upload from CI, not just a local build.
npx cap run ios --target <device-id>
npx cap run android --target <device-id>
./gradlew :app:bundleRelease # from android/, catches release-only issues
Release builds catch what debug builds hide: R8 rules from a removed plugin, missing entitlements after the workspace change, and signing steps that still reference the old workspace path.
A migration checklist you can paste into a ticket
-
cap doctor+ plugin inventory captured - CI images on Xcode 16+ / JDK 21
- JS layer upgraded and browser build green
-
npx cap migraterun;CapApp-SPM/Package.swiftreviewed - Pods removed; fastlane/CI switched from workspace to project
-
Package.resolvedcommitted - AGP / Gradle / SDK 36 / JDK 21 set;
namespacepresent everywhere - Every flagged plugin upgraded, replaced, forked or absorbed
- Device smoke test: push, deep links, camera, hardware
- Signed release build uploaded from CI
Done in one branch, this is a boring upgrade. Left for another year, it becomes a rewrite, because the next OS release will force the toolchain move anyway and you will be doing it under a store deadline.
If you would rather not spend your team's sprint on Gradle files, this is exactly the kind of work we take end to end — see legacy Ionic app modernization or talk to us about an upgrade audit.