CocoaPods to SwiftPM Migration for KMP
Migrate Kotlin Multiplatform projects from
kotlin("native.cocoapods")
to
DSL.
Requirements
- Kotlin: Version with Swift Import support (e.g., 2.4.0-Beta1 or later)
- Xcode: 16.4 or 26.0+
- iOS Deployment Target: 16.0+ recommended
Migration Overview
IMPORTANT: Keep the
block and plugin active until Phase 6. The migration adds
alongside the existing CocoaPods setup first, reconfigures Xcode, and only then removes CocoaPods.
| Phase | Action |
|---|
| 1 | Analyze existing CocoaPods configuration |
| 2 | Update Gradle configuration (repos, Kotlin version) |
| 3 | Add alongside existing |
| 4 | Transform Kotlin imports |
| 5 | Reconfigure iOS project and deintegrate CocoaPods |
| 6 | Remove CocoaPods plugin from Gradle |
| 7 | Verify Gradle build and Xcode project build |
| 8 | Write MIGRATION_REPORT.md |
Phase 1: Pre-Migration Analysis
1.0 Verify the project builds
Before starting migration, identify the module to migrate and confirm it compiles successfully.
-
Find the module that uses CocoaPods — look for
files containing
:
bash
grep -rl "cocoapods" --include="build.gradle.kts" .
Extract the module name from the path (e.g.,
./shared/build.gradle.kts
→ module name is
).
-
Build only that module (avoids building the entire multi-module project):
bash
./gradlew :moduleName:build
Replace
with the directory name of the module (e.g.,
).
-
If the targeted build fails, ask the user to either:
- Provide the correct Gradle command to verify the module builds, or
- Confirm the module is in a working state and it's safe to proceed
If the user confirms without providing a build command, record that the pre-migration build could not be verified and warn about this at the end of migration (Phase 7).
1.0a Confirm Kotlin version with Swift Import support
Ask the user:
Does your project already use a Kotlin version with Swift Import support (swiftPMDependencies DSL)?
If yes → read their current Kotlin version from
gradle/libs.versions.toml
(or
), record it, and skip Phase 2.2 (no version change needed).
If no → ask:
Please provide the Kotlin version to use (e.g., "2.4.0", "2.4.0-Beta1", "2.4.0-dev-123").
Record the user-provided version. Then ask:
Does this Kotlin version require a custom Maven repository (e.g., JetBrains dev repo)?
- Yes → ask for the repo URL (suggest
https://packages.jetbrains.team/maven/p/kt/dev
as default). Phase 2.1 will add it.
- No → Phase 2.1 is skipped (no custom repo needed).
Finally, check the project's current Kotlin version. Compare
major.minor against the target. If it differs significantly (e.g.,
→
), warn: "⚠️ Kotlin version jump — upgrading across minor versions can introduce breaking changes unrelated to this migration. Recommended: update first, verify it builds, then re-run." If the user confirms despite the mismatch, proceed.
1.1 Check for deprecated CocoaPods workaround property
Search
for the deprecated property:
properties
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true
This property was a workaround (see
KT-64096) for projects using
alongside CocoaPods dependencies. It suppresses an error about unsupported configurations that can cause runtime crashes or symbol duplication. After migrating to SwiftPM import, this property is no longer needed and
must be removed in Phase 6. Record its presence if found.
1.2 Check for EmbedAndSign disablers
Search all
files for code that disables
tasks (e.g.,
filters,
blocks). This is a CocoaPods-era workaround that
breaks the migration because
(needed in Phase 5) gets disabled too. Record any such code — it
must be removed in Phase 6, and may need to be removed earlier. See
troubleshooting.md § "
Skipped" for patterns.
1.3 Check for third-party KMP libraries with bundled cinterop klibs
Some KMP libraries ship pre-built cinterop klibs with
package namespaces. After migration, the swiftPMDependencies cinterop generator detects these existing bindings and
skips generating new bindings for those Clang modules to avoid duplicates. This means
imports for those modules must be
kept as-is — they resolve to the third-party library's bundled klib, not to actual CocoaPods.
Known libraries with bundled klibs:
| Library | Maven artifact | Bundled klib namespace | Classes provided |
|---|
| KMPNotifier | io.github.mirzemehdi:kmpnotifier
| cocoapods.FirebaseMessaging
| , FIRMessagingAPNSTokenType
, etc. |
How to detect: Search Gradle dependency declarations for known libraries, then cross-reference their bundled namespaces against the
statements found in step 4. Mark any matches — these imports will NOT be transformed in Phase 4.
If unsure whether a third-party KMP library bundles cinterop klibs, check if it has a
pod dependency in the project — this is a strong indicator that the library provides its own klib for those classes.
To inspect klib contents and verify bundled bindings, see troubleshooting.md § "Third-Party KMP Libraries with Bundled Klibs".
Find and record:
- CocoaPods configuration - Search for in files
- Pod dependencies - Extract pod names, versions from blocks
- Framework configuration - Record , , deployment target from
- linkOnly pods - Record pods declared with . These pods provide native linking only — cinterop bindings come from a KMP wrapper library (e.g., ). See common-pods-mapping.md for implications.
- Kotlin imports - Find all statements. Cross-reference with step 1.3 to identify which imports come from bundled klibs (and must be preserved) vs. which come from direct pod cinterop (and must be transformed).
- Map pods to SPM - See common-pods-mapping.md
- Locate iOS project directory - Find the directory containing and :
bash
find . -name "Podfile" -type f
Record this path (e.g., , , or project root) - needed for Phase 5
- Check for non-KMP CocoaPods - Determine if the project uses CocoaPods for dependencies other than KMP. This affects cleanup strategy in Phase 5.
- Check Xcode build phases - Open the 's and search for the Gradle build phase script. Check if
embedAndSignAppleFrameworkForXcode
is present but commented out (prefixed with ). If commented out, it must be uncommented during Phase 5 — the task may or may not handle this automatically.
- Check for existing Crashlytics dSYM upload script - If using FirebaseCrashlytics, search for a dSYM upload shell script phase. Record its current path (CocoaPods-era scripts reference
${PODS_ROOT}/FirebaseCrashlytics/upload-symbols
). This must be updated to the SPM path in Phase 5.
- Identify CocoaPods-related extras in build scripts - Search all files for CocoaPods workarounds beyond the standard block (custom tasks hooking into , patching, podspec metadata, , , etc.). See cocoapods-extras-patterns.md for the full pattern list. Record all findings — these will be handled in Phase 6.
Phase 2: Gradle Configuration
Important scope note: Do NOT upgrade the Gradle wrapper version, update KSP, or update any other dependencies during this migration. Those are separate concerns and out of scope. Only change what is listed below.
2.1 Add custom Maven repository (if needed)
Skip this step if the user indicated in Phase 1.0a that their Kotlin version does not require a custom Maven repository (i.e., it is an official release, Beta, or RC available from Maven Central).
For dev/custom builds, add the custom Maven repository (URL from Phase 1.0a) to
:
kotlin
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
maven("<custom-repo-url>") // ADD
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("<custom-repo-url>") // ADD
}
}
2.2 Update Kotlin version
Skip this step if the user's project already uses a Kotlin version with Swift Import support (recorded in Phase 1.0a).
Update to the version recorded in Phase 1.0a:
toml
# gradle/libs.versions.toml
[versions]
kotlin = "<kotlin-version>"
2.3 Add buildscript constraint (if swiftPMDependencies not recognized)
kotlin
// root build.gradle.kts
buildscript {
dependencies.constraints {
"classpath"("org.jetbrains.kotlin:kotlin-gradle-plugin:<kotlin-version>!!")
}
}
Replace
with the version recorded in Phase 1.0a. The
suffix forces strict version resolution, ensuring no other dependency pulls in a different Kotlin Gradle plugin version.
Phase 3: Add swiftPMDependencies (Keep CocoaPods)
Do NOT remove the block or kotlin("native.cocoapods")
plugin yet. Add
alongside the existing CocoaPods configuration.
3.1 Add group property
kotlin
group = "org.example.myproject" // Required for import namespace
3.2 Add swiftPMDependencies block alongside cocoapods
For each pod dependency, add the equivalent SwiftPM package declaration. Use
common-pods-mapping.md to map each pod to its SPM package URL, product name, and
.
Key concepts: = SPM product names (controls linking).
= Clang module names for cinterop bindings (only when
discoverModulesImplicitly = false
).
discoverModulesImplicitly
defaults to
(bindings for all Clang modules); set
when transitive C/C++ modules fail cinterop (Firebase, gRPC), then list needed modules explicitly.
Important: SPM product names and Clang module names don't always match. Always consult common-pods-mapping.md for correct values.
Do not mix the same library suite across CocoaPods and SPM. Libraries that share a common repository (e.g., all Firebase products) share transitive dependencies. Having some products linked via CocoaPods and others via SPM causes duplicate/conflicting symbols and dyld crashes at runtime. When migrating such a suite, move
all pods from that suite to SPM at once — including Swift-only pods that Kotlin doesn't use directly. Add Swift-only pods as
entries (no
needed). After adding new products, re-run
to regenerate the linkage Swift package.
kotlin
kotlin {
// Keep existing targets
iosArm64()
iosSimulatorArm64()
iosX64()
swiftPMDependencies {
iosDeploymentVersion.set("16.0")
// If using KMP IntelliJ plugin, specify the .xcodeproj path:
// xcodeProjectPathForKmpIJPlugin.set(
// layout.projectDirectory.file("../iosApp/iosApp.xcodeproj")
// )
swiftPackage(
url = url("https://github.com/owner/repo.git"),
version = from("1.0.0"),
products = listOf(product("ProductName")),
)
}
cocoapods {
// ... keep existing cocoapods block for now
}
}
3.3 Move framework configuration out of cocoapods block
If the
block contains a
configuration, move it to the
API on each target.
is recommended — dynamic frameworks have known edge cases with SwiftPM import that can cause linker errors, dyld crashes, or duplicate class warnings:
kotlin
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { iosTarget ->
iosTarget.binaries.framework { baseName = "Shared"; isStatic = true }
}
3.4 Handle dev.gitlive/firebase-kotlin-sdk and similar CocoaPods-era KMP wrappers
If the project uses
or similar KMP wrapper libraries, two additional steps are required:
A. Switch to — dynamic frameworks + Firebase SPM = runtime
crash. After switching: re-run
, remove any "Embed Frameworks" copy phase, move linker flags to
.
B. Add framework search paths — add conditional
linkerOpts in
and matching
in the Xcode project.
See common-pods-mapping.md § dev.gitlive and troubleshooting.md for code snippets and the full product list.
3.5 Add opt-in for cinterop API
kotlin
kotlin.compilerOptions {
optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
}
For full DSL reference, see dsl-reference.md.
Phase 4: Kotlin Source Updates
Import Namespace Formula
swiftPMImport.<group>.<module>.<ClassName>
Where:
- group: build.gradle.kts `group` property, dashes (-) → dots (.)
- module: Gradle module name, dashes (-) → dots (.)
- ClassName: Objective-C class name (FIR* for Firebase, GMS* for Google Maps)
Example Transformation
kotlin
// group = "org.jetbrains.kotlin.firebase.sample", module = "kotlin-library"
// BEFORE:
import cocoapods.FirebaseAnalytics.FIRAnalytics
// AFTER:
import swiftPMImport.org.jetbrains.kotlin.firebase.sample.kotlin.library.FIRAnalytics
Import flattening: The Clang module name (e.g.,
FirebaseFirestoreInternal
,
) disappears from the import path — all classes are flattened under the same
swiftPMImport.<group>.<module>
prefix regardless of which library they come from. For example, both
cocoapods.FirebaseAuth.FIRAuth
and
cocoapods.FirebaseFirestoreInternal.FIRFirestore
become
swiftPMImport.<group>.<module>.FIRAuth
and
swiftPMImport.<group>.<module>.FIRFirestore
.
Preserving Bundled Klib Imports
CRITICAL: Do NOT replace
imports that resolve to third-party KMP libraries' bundled cinterop klibs (identified in Phase 1 step 1.3). These imports must remain as-is — the
prefix is the package namespace in the library's published klib, not an actual CocoaPods dependency. The swiftPMDependencies cinterop generator skips modules already provided by a dependency's klib, so
for those classes will fail with "Unresolved reference".
Example (project using
KMPNotifier):
kotlin
// KEEP — resolves to kmpnotifier's bundled cinterop klib
import cocoapods.FirebaseMessaging.FIRMessaging
Bulk Replacement
Use a regex find-and-replace across all Kotlin source files, excluding imports identified in Phase 1 step 1.3:
Find: cocoapods\.\w+\.
Replace: swiftPMImport.<your.group>.<your.module>.
After bulk replacement,
manually restore any
imports that should be preserved (from bundled klibs).
Finding correct import path: Run
./gradlew :moduleName:build
- errors show available classes.
Phase 5: iOS Project Reconfiguration
5.1 Get migration command
Build the CocoaPods workspace to obtain the migration command:
bash
cd /path/to/iosApp
xcodebuild -scheme "$(echo -n *.xcworkspace | python3 -c 'import sys, json; from subprocess import check_output; print(list(set(json.loads(check_output(["xcodebuild", "-workspace", sys.stdin.readline(), "-list", "-json"]))["workspace"]["schemes"]) - set(json.loads(check_output(["xcodebuild", "-project", "Pods/Pods.xcodeproj", "-list", "-json"]))["project"]["schemes"]))[0])')" -workspace *.xcworkspace -destination 'generic/platform=iOS Simulator' ARCHS=arm64 | grep -A5 'What went wrong'
The build output will contain a command like:
bash
XCODEPROJ_PATH='/path/to/project/iosApp.xcodeproj' GRADLE_PROJECT_PATH=':shared' '/path/to/project/gradlew' -p '/path/to/project' ':shared:integrateEmbedAndSign' ':shared:integrateLinkagePackage'
Run this command. It modifies the
to trigger
embedAndSignAppleFrameworkForXcode
during the build.
is a one-time setup — it does not need to be added as a build phase. If
is skipped, check for EmbedAndSign disablers (Phase 1 step 1.2) — remove them first, then re-run.
Verify embedAndSignAppleFrameworkForXcode
is active: After running integration, check the build phase script in
. If
embedAndSignAppleFrameworkForXcode
is commented out (prefixed with
), uncomment it.
The
task generates
_internal_linkage_SwiftPMImport/
at
— a local Swift package that mirrors your
list and ensures SPM libraries are linked into the final binary.
After running the integration tasks,
disable User Script Sandboxing (
ENABLE_USER_SCRIPT_SANDBOXING = NO
) in the
. Xcode 16+ enables it by default, which prevents the Gradle build phase from writing to the project directory:
bash
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' "$XCODEPROJ_PATH/project.pbxproj"
If the setting is absent (Xcode defaults to YES), add
ENABLE_USER_SCRIPT_SANDBOXING = NO;
to the app target's
sections. Then restart the Gradle daemon:
Alternative (if xcodebuild approach fails): See troubleshooting.md § "Manual Integration Command Discovery" for a fallback script to discover paths and run integration tasks directly.
5.2 Update Crashlytics dSYM upload script (if applicable)
If the project uses FirebaseCrashlytics and has a dSYM upload run script phase (identified in Phase 1 step 10), update the script path from
${PODS_ROOT}/FirebaseCrashlytics/upload-symbols
to
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"
. See
troubleshooting.md § "Firebase Crashlytics: dSYM Upload Script" and
common-pods-mapping.md for the full script and input files list.
5.3 Deintegrate CocoaPods
Option A: Full deintegration (if CocoaPods was used ONLY for KMP dependencies):
Before deleting files, run
and verify the paths. If unsure, move files to a backup location instead of deleting immediately.
bash
cd /path/to/iosApp
pod deintegrate
rm -rf Podfile Podfile.lock Pods/
# Remove the workspace that matches your app xcodeproj name
XCODEPROJ_NAME=$(basename "$(find . -maxdepth 1 -name "*.xcodeproj" -type d | grep -v Pods | head -1)" .xcodeproj)
rm -rf "${XCODEPROJ_NAME}.xcworkspace"
# Return to project root
cd ..
# Remove the migrated module podspec only (for example, shared.podspec)
# If unknown, list candidates and remove the matching one explicitly:
ls -1 *.podspec
# rm -f shared.podspec
This cleanup snippet is self-contained and does not assume
or
from the earlier one-off migration command are still available in your shell.
If
is not available, see
troubleshooting.md § "Manual CocoaPods Deintegration from pbxproj" for the full list of references to remove. Also remove
from
and delete the
directory.
Option B: Partial removal (if other non-KMP CocoaPods dependencies remain):
Remove only the KMP pod line from the
and re-run pod install:
ruby
target 'iosApp' do
# Remove this line:
pod 'shared', :path => '../shared'
# Keep other non-KMP pods
end
bash
cd /path/to/iosApp && pod install
Tip: Consider migrating remaining pods to SPM too — most popular iOS libraries support it natively. Add them in Xcode via File → Add Package Dependencies, then fully deintegrate CocoaPods once all pods are replaced.
5.4 Manual integration (if automatic fails)
See troubleshooting.md § "Manual Xcode Integration Steps" for the 5-step manual setup (build phase, sandboxing, linkage package).
Phase 6: Remove CocoaPods from Gradle
Now that the iOS project is reconfigured, remove the CocoaPods plugin and block:
6.1 Remove CocoaPods plugin
kotlin
plugins {
// REMOVE: kotlin("native.cocoapods")
alias(libs.plugins.kotlinMultiplatform) // Keep
}
6.2 Remove cocoapods block
Delete the entire
block from
. The
block and
configuration added in Phase 3 replace it.
6.3 Remove deprecated gradle.properties entries
If found in Phase 1.1, remove from
:
properties
# REMOVE — no longer needed after migrating away from CocoaPods (KT-64096)
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true
6.4 Clean up CocoaPods-related extras
Review the extras identified in Phase 1 step 11. Podspec metadata,
, CocoaPods task hooks, and
patching code are
safe to remove without user consultation. Non-standard pod configurations (
,
), custom cinterop
setups, and CocoaPods-specific compiler/linker flags
require analysis — consult the user if unsure whether SPM handles them automatically.
See cocoapods-extras-patterns.md for the full categorized list with examples.
Phase 7: Verification
7.1 Build Gradle project
Build the migrated module to verify the migration succeeded:
bash
./gradlew :moduleName:build
7.2 Link framework
bash
./gradlew :moduleName:linkDebugFrameworkIosSimulatorArm64
7.3 Build iOS/macOS Xcode project
After the Gradle build succeeds, build the Xcode project. Use
if all CocoaPods were removed (Option A), or
if non-KMP CocoaPods remain (Option B):
bash
cd /path/to/iosApp
# Discover schemes and build (replace -project/-workspace as needed; for macOS use -destination 'platform=macOS'):
xcodebuild -project *.xcodeproj -list -json 2>/dev/null | python3 -c "import sys,json; schemes=json.load(sys.stdin)['project']['schemes']; [print(s) for s in schemes]"
xcodebuild -project *.xcodeproj -scheme "<AppScheme>" -destination 'generic/platform=iOS Simulator' ARCHS=arm64 build
If checkSandboxAndWriteProtection
fails — sandboxing was not disabled in Phase 5.1. Go back and apply the sandboxing fix from Phase 5.1, then retry.
If the pre-migration build was not verified (Phase 1.0 fallback was used), warn the user:
Note: The pre-migration build could not be fully verified. If build errors appear now, some may be pre-existing issues unrelated to the migration. Compare errors against the pre-migration build output to distinguish migration issues from prior problems.
If the build fails
Do NOT revert the migration. Read the error log, re-check Phases 2-6, and consult troubleshooting.md. If unsure, present options to the user — do not silently undo migration work.
Phase 8: Migration Report
After migration (whether successful or not), write a comprehensive
in the project root. Use the template in
migration-report-template.md.
The report must include:
- Pre-Migration State — CocoaPods dependencies (name, version, ), framework config, imports, non-KMP pods, atypical configuration
- Migration Steps — exact changes per phase with before/after snippets for non-trivial changes
- Import Transformations — table of every import change, clearly marking preserved imports and which bundled klib provides them
- Errors Encountered — structured entries: phase, exact symptom, root cause, fix, generalizable flag
- Non-Trivial Decisions — changes, preserved imports, framework search paths, trade-offs
- Files Changed — complete list grouped by type (Gradle, Kotlin, Xcode, created, deleted)
Additional Resources
- DSL Reference - Full swiftPMDependencies syntax
- Common Pods Mapping - Pod to SPM mapping table
- CocoaPods Extras Patterns - Detection and cleanup patterns for CocoaPods workarounds
- Troubleshooting - Issues, solutions, rollback
- Migration Report Template - Post-migration report template