
Kotlin Tooling Cocoapods Spm Migration
- 995 installs
- 983 repo stars
- Updated July 21, 2026
- kotlin/kotlin-agent-skills
kotlin-tooling-cocoapods-spm-migration is a JetBrains Kotlin agent skill that migrates Kotlin Multiplatform projects from CocoaPods kotlin("native.cocoapods") to the Swift Package Manager swiftPMDependencies DSL for deve
About
kotlin-tooling-cocoapods-spm-migration is version 1.0.0 in kotlin/kotlin-agent-skills and guides migration from CocoaPods to Swift Package Manager in KMP repos. The skill replaces pod() declarations with swiftPackage(), transforms cocoapods.* imports to swiftPMImport.*, and reconfigures the Xcode project for SPM resolution. It requires Kotlin 2.4.0-Beta2 or later with swiftPMDependencies support on Maven Central and Xcode 16.4 or 26.0+. Developers reach for this skill when Apple-ecosystem pressure or build pain makes CocoaPods untenable but the shared Kotlin layer must keep shipping iOS builds with correct native linkage.
- 8-phase migration checklist
- Transforms cocoapods imports to swiftPMImport
- Produces MIGRATION_REPORT.md on completion
Kotlin Tooling Cocoapods Spm Migration by the numbers
- 995 all-time installs (skills.sh)
- +57 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #181 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-cocoapods-spm-migrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 995 |
|---|---|
| repo stars | ★ 983 |
| Last updated | July 21, 2026 |
| Repository | kotlin/kotlin-agent-skills ↗ |
How do you migrate KMP from CocoaPods to Swift Package Manager?
Install this skill to migrate an existing Kotlin Multiplatform project from CocoaPods to Swift Package Manager.
Who is it for?
Mobile developers on Kotlin Multiplatform iOS targets who must move native dependencies from CocoaPods to Swift Package Manager on Kotlin 2.4.0-Beta2+.
Skip if: Pure Android apps, greenfield KMP projects already on SPM, or teams still pinned below Kotlin 2.4.0-Beta2 without swiftPMDependencies support.
When should I use this skill?
The user asks to migrate a KMP project from CocoaPods to SPM, replace pod() with swiftPackage(), or fix cocoapods imports after an Xcode upgrade.
What you get
Updated build.gradle.kts swiftPMDependencies block, swiftPMImport mappings, and reconfigured Xcode project using SPM instead of CocoaPods.
- Updated swiftPMDependencies Gradle configuration
- swiftPMImport dependency mappings
- Reconfigured Xcode project for SPM
By the numbers
- Skill metadata version 1.0.0
- Requires Kotlin 2.4.0-Beta2 or later
- Targets Xcode 16.4 or 26.0+
Files
CocoaPods to SwiftPM Migration for KMP
Migrate Kotlin Multiplatform projects from kotlin("native.cocoapods") to swiftPMDependencies {} DSL.
Requirements
- Kotlin: 2.4.0-Beta2 or later (first public release with
swiftPMDependenciessupport, available on Maven Central) - Xcode: 16.4 or 26.0+
- iOS Deployment Target: 16.0+ recommended
Migration Overview
IMPORTANT: Keep the cocoapods {} block and plugin active until Phase 6. The migration adds swiftPMDependencies {} 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 swiftPMDependencies {} alongside existing cocoapods {} |
| 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.
1. Find the module that uses CocoaPods — look for build.gradle.kts files containing cocoapods:
grep -rl "cocoapods" --include="build.gradle.kts" .Extract the module name from the path (e.g., ./shared/build.gradle.kts → module name is shared). Note: multiple modules may use CocoaPods — record all of them. Typically only the module that produces the framework linked into the iOS app needs swiftPMDependencies; the others only need CocoaPods removed (Phase 6).
2. Compile Kotlin code — run the Kotlin compilation task for that module to verify the Kotlin source compiles:
./gradlew :moduleName:compileKotlinIosSimulatorArm64Replace moduleName with the directory name of the module (e.g., :shared:compileKotlinIosSimulatorArm64). This is faster than a full build (which also runs release linkage) and sufficient to verify Kotlin code correctness.
3. Build the iOS app (optional) — try to locate the Xcode project and build it to confirm the full app compiles:
# Find the Xcode project
find . -name "*.xcworkspace" -not -path "*/Pods/*" -maxdepth 2
# Build (replace scheme name with the actual app scheme)
cd /path/to/iosApp
xcodebuild -workspace *.xcworkspace -scheme "<AppScheme>" -destination 'generic/platform=iOS Simulator' ARCHS=arm64If the user wants to skip the Xcode build or no Xcode project is found, proceed without it — the Kotlin compilation from step 2 is sufficient to continue.
4. If the Kotlin compilation 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
Read the current Kotlin version from gradle/libs.versions.toml (or build.gradle.kts).
If the project already uses Kotlin 2.4.0-Beta2 or later → record the version and skip Phase 2.1 (no version change needed).
If the project uses an older Kotlin version → Phase 2.1 will upgrade it to 2.4.0-Beta2 (the first public release with swiftPMDependencies support, available on Maven Central — no custom repository needed). Warn the user: "⚠️ Kotlin version jump — upgrading across minor versions can introduce breaking changes unrelated to this migration. Recommended: update first, verify it builds, then re-run this migration." If the user confirms, proceed.
1.1 Check for deprecated CocoaPods workaround property
Search gradle.properties for the deprecated property:
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=trueThis property was a workaround (see KT-64096) for projects using embedAndSign 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 build.gradle.kts files for code that disables EmbedAndSign tasks (e.g., TaskGraph.whenReady filters, tasks.matching blocks). This is a CocoaPods-era workaround that breaks the migration because integrateEmbedAndSign (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 § "integrateEmbedAndSign Skipped" for patterns.
1.3 Check for third-party KMP libraries with bundled cinterop klibs
Some KMP libraries ship pre-built cinterop klibs with cocoapods.* 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 cocoapods.* 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 `cocoapods.` klibs:**
| Library | Maven artifact | Bundled klib namespace | Classes provided |
|---|---|---|---|
| KMPNotifier | io.github.mirzemehdi:kmpnotifier | cocoapods.FirebaseMessaging | FIRMessaging, FIRMessagingAPNSTokenType, etc. |
How to detect: Search Gradle dependency declarations for known libraries, then cross-reference their bundled namespaces against the import cocoapods.* 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 linkOnly = true 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:
1. CocoaPods configuration - Search for cocoapods in build.gradle.kts files 2. Pod dependencies - Extract pod names, versions from cocoapods {} blocks 3. Framework configuration - Record baseName, isStatic, deployment target from cocoapods.framework {} 4. linkOnly pods - Record pods declared with linkOnly = true. These have two common patterns:
- KMP wrapper libraries (e.g.,
dev.gitlive:firebase-*): the wrapper provides Kotlin APIs, and the pod is only linked. See common-pods-mapping.md for implications. - Multi-module projects: the consuming module declares
linkOnly = truebecause a child module already provides cinterop bindings for that pod. In SwiftPM, theswiftPackage()declaration should go only in the child module that uses the pod directly. The consuming module must NOT redeclare the same packages — it only needs aswiftPMDependencies {}block without those packages (or an empty one if all pods werelinkOnly). Import namespace implication: when the consuming module imports SPM classes that come from a child module'sswiftPMDependencies, the import path uses the child module's group and name as the namespace (see Phase 4 Import Namespace Formula).
5. Kotlin imports - Find all import cocoapods.* 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). 6. Map pods to SPM - See common-pods-mapping.md 7. Locate iOS project directory - Find the directory containing Podfile and .xcworkspace:
find . -name "Podfile" -type fRecord this path (e.g., iosApp/, ios/, or project root) - needed for Phase 5 8. Check for non-KMP CocoaPods - Determine if the project uses CocoaPods for dependencies other than KMP. This affects cleanup strategy in Phase 5. 9. Cross-reference Podfile against `cocoapods {}` block - Parse the Podfile and compare its pod entries with the pods declared in the Gradle cocoapods {} block. Record any dependencies that exist in the Podfile but are not listed in cocoapods {}. These Podfile-only dependencies still linked into the app via CocoaPods and must be migrated to swiftPMDependencies — dropping them silently causes obscure linkage errors at runtime. 10. Check Xcode build phases - Open the .xcodeproj's project.pbxproj 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 integrateEmbedAndSign task may or may not handle this automatically. 11. Check for existing Crashlytics dSYM upload script - If using FirebaseCrashlytics, search project.pbxproj 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. 12. Identify CocoaPods-related extras in build scripts - Search all build.gradle.kts files for CocoaPods workarounds beyond the standard cocoapods {} block (custom tasks hooking into podInstall, Pods.xcodeproj patching, podspec metadata, extraSpecAttributes, noPodspec(), 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 Update Kotlin version
Skip this step if the project already uses Kotlin 2.4.0-Beta2 or later (recorded in Phase 1.0a).
Update to 2.4.0-Beta2 (or the latest available release with Swift Import support) in gradle/libs.versions.toml:
[versions]
kotlin = "2.4.0-Beta2"2.4.0-Beta2 is available on Maven Central — no custom repository is needed.
---
Phase 3: Add swiftPMDependencies (Keep CocoaPods)
Do NOT remove the `cocoapods {}` block or `kotlin("native.cocoapods")` plugin yet. Add swiftPMDependencies {} alongside the existing CocoaPods configuration.
3.1 Add group property
group = "org.example.myproject" // Required for import namespaceCompose Resources warning: If the project uses Compose Multiplatform resources (org.jetbrains.compose plugin or compose.resources), the group property is also used as the namespace for generated resource accessors (e.g., Res.string.*, Res.drawable.*). If group already exists in build.gradle.kts, do not change it. If you are adding group for the first time, warn the user that existing Compose resource accessor call sites throughout the project will change namespace and may need updating.
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 importedClangModules.
Version preservation: Do NOT bump dependency versions during migration. Use the exact same version that was specified in the cocoapods {} block. Changing versions can resolve to different library builds that break cinterop APIs (removed symbols, changed signatures) and introduce issues unrelated to the migration itself.
| CocoaPods version spec | SPM equivalent | Example |
|---|---|---|
version = "1.2.3" (exact) | version = "1.2.3" (simple) or exact("1.2.3") (typed) | pod("GoogleMaps") { version = "10.3.0" } → version = "10.3.0" |
version = "~> 1.2" (optimistic) | version = "1.2.0" (simple) or from("1.2.0") (typed) | pod("FirebaseAuth") { version = "~> 12.5" } → version = "12.5.0" |
| No version specified | Ask user which version to pin | Ask the user which version to use |
Two API forms: The DSL has a simple string API and a typed API. Use the simple string API for most packages:
swiftPackage(url = "https://github.com/owner/repo.git", version = "1.0.0", products = listOf("ProductName"))The simple API auto-defaults importedClangModules to the products list. Use the typed API (with url(), exact(), product() wrappers) only when you need exact version pinning, platform constraints, or explicit Clang module control. See dsl-reference.md for the typed API.
Key concepts: products = SPM product names (controls linking). importedClangModules = Clang module names for cinterop bindings (only when discoverClangModulesImplicitly = false). discoverClangModulesImplicitly defaults to true (bindings for all Clang modules); set false 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.
Podfile-only dependencies: If Phase 1 step 9 identified dependencies that exist in the Podfile but not in the Gradle cocoapods {} block, these must also be added to swiftPMDependencies as products entries. Even though the KMP module didn't declare them, they were linked into the app by CocoaPods and may be required for the app to build. Look up each Podfile-only pod's SPM package URL and add it as a swiftPackage() with at least its products. If any of these pods were used via cinterop (check for import cocoapods.* statements referencing them), also add importedClangModules.
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 products entries (no importedClangModules needed). After adding new products, re-run integrateLinkagePackage to regenerate the linkage Swift package.
kotlin {
// Keep existing targets
iosArm64()
iosSimulatorArm64()
iosX64()
swiftPMDependencies {
iosMinimumDeploymentTarget = "16.0"
swiftPackage(
url = "https://github.com/owner/repo.git",
version = "1.0.0",
products = listOf("ProductName"),
)
}
cocoapods {
// ... keep existing cocoapods block for now
}
}3.3 Move framework configuration out of cocoapods block
If the cocoapods block contains a framework {} configuration, move it to the binaries API on each target. `isStatic = true` is recommended — dynamic frameworks have known edge cases with SwiftPM import that can cause linker errors, dyld crashes, or duplicate class warnings:
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { iosTarget ->
iosTarget.binaries.framework { baseName = "Shared"; isStatic = true }
}If the cocoapods.framework {} block contained export(project(...)) or transitiveExport = true, preserve these in the new binaries.framework {} block — they are essential for multi-module projects where the framework exports child modules.
3.4 Handle dev.gitlive/firebase-kotlin-sdk and similar CocoaPods-era KMP wrappers
If the project uses dev.gitlive:firebase-* or similar KMP wrapper libraries, two additional steps are required:
A. Switch to `isStatic = true` — dynamic frameworks + Firebase SPM = runtime dyld crash. After switching: re-run integrateLinkagePackage, remove any "Embed Frameworks" copy phase, move linker flags to OTHER_LDFLAGS.
B. Add framework search paths — add conditional -F linkerOpts in build.gradle.kts and matching FRAMEWORK_SEARCH_PATHS 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 annotations
The swiftPackage() and localSwiftPackage() DSL functions are annotated with @ExperimentalKotlinGradlePluginApi. Add this opt-in to suppress the compiler warning:
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)Place this at the top of each build.gradle.kts file that calls swiftPackage() or localSwiftPackage().
Also add the cinterop opt-in for Kotlin source files:
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 of the MODULE THAT DECLARES the swiftPMDependencies, dashes (-) → dots (.)
- module: Gradle module name of the MODULE THAT DECLARES the swiftPMDependencies, dashes (-) → dots (.), underscores (_) preserved as-is
- ClassName: Objective-C class name (FIR* for Firebase, GMS* for Google Maps)The namespace uses the declaring module's group+name, not the importing module's. This is the most common mistake agents make. When module A depends on module B, and module B declares swiftPMDependencies, module A imports SPM classes using module B's group and module name — NOT module A's.
Example Transformation — Single Module
// group = "org.jetbrains.kotlin.firebase.sample", module = "kotlin-library"
// BEFORE:
import cocoapods.FirebaseAnalytics.FIRAnalytics
// AFTER:
import swiftPMImport.org.jetbrains.kotlin.firebase.sample.kotlin.library.FIRAnalyticsExample Transformation — Multi-Module (linkOnly pods)
When a consuming module had pod("GoogleMaps") { linkOnly = true } because a child module provides the cinterop bindings:
// composeApp/App.kt — composeApp depends on :google-maps Gradle module
// google-maps has group = "org.jetbrains.kotlin.google-maps", module name = "google-maps"
// google-maps declares swiftPMDependencies with GoogleMaps
// BEFORE (in composeApp):
import cocoapods.GoogleMaps.GMSServices
// AFTER — uses google-maps module's namespace, NOT composeApp's namespace:
import swiftPMImport.org.jetbrains.kotlin.google.maps.google.maps.GMSServices
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^
// google-maps's group (dashes→dots) google-maps's module name (dashes→dots)The import path does NOT use composeApp's group (org.jetbrains.kotlin.compose.sample). It uses the declaring module's identity because that's where the cinterop bindings are generated.
Import flattening: The Clang module name (e.g., FirebaseFirestoreInternal, FirebaseAuth) 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 replacecocoapods.*imports that resolve to third-party KMP libraries' bundled cinterop klibs (identified in Phase 1 step 1.3). These imports must remain as-is — thecocoapodsprefix 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, soswiftPMImport.*for those classes will fail with "Unresolved reference".
Example (project using KMPNotifier):
// KEEP — resolves to kmpnotifier's bundled cinterop klib
import cocoapods.FirebaseMessaging.FIRMessagingBulk Replacement
Use a regex find-and-replace across all Kotlin source files, excluding imports identified in Phase 1 step 1.3. In multi-module projects, run the replacement separately for each module using that module's group and name:
Find: cocoapods\.\w+\.
Replace: swiftPMImport.<declaring.module.group>.<declaring.module.name>.After bulk replacement, manually restore any cocoapods.* imports that should be preserved (from bundled klibs).
Finding correct import path: Run ./gradlew :moduleName:compileKotlinIosSimulatorArm64 - errors show available classes.
---
Phase 5: iOS Project Reconfiguration
5.1 Get migration command
Build the CocoaPods workspace to obtain the migration command:
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:
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 .xcodeproj to trigger embedAndSignAppleFrameworkForXcode during the build. integrateLinkagePackage is a one-time setup — it does not need to be added as a build phase. If integrateEmbedAndSign 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 project.pbxproj. If embedAndSignAppleFrameworkForXcode is commented out (prefixed with #), uncomment it.
The integrateLinkagePackage task generates KotlinMultiplatformLinkedPackage/ at <iosDir>/ — a local Swift package that mirrors your products 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 .xcodeproj. Xcode 16+ enables it by default, which prevents the Gradle build phase from writing to the project directory:
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 buildSettings sections. Then restart the Gradle daemon: ./gradlew --stop
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 11), 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 git status --short and verify the paths. If unsure, move files to a backup location instead of deleting immediately.
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.podspecThis cleanup snippet is self-contained and does not assume XCODEPROJ_PATH or GRADLE_PROJECT_PATH from the earlier one-off migration command are still available in your shell.
If pod deintegrate is not available, see troubleshooting.md § "Manual CocoaPods Deintegration from pbxproj" for the full list of references to remove. Also remove Pods/ from .gitignore and delete the .xcworkspace directory.
Option B: Partial removal (if other non-KMP CocoaPods dependencies remain):
Remove only the KMP pod line from the Podfile and re-run pod install:
target 'iosApp' do
# Remove this line:
pod 'shared', :path => '../shared'
# Keep other non-KMP pods
endcd /path/to/iosApp && pod installTip: 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 from all modules that used it (not just the primary one):
6.1 Remove CocoaPods plugin
Remove kotlin("native.cocoapods") or alias(libs.plugins.kotlinCocoapods) from plugins {} in every module that used it:
plugins {
// REMOVE: kotlin("native.cocoapods") or alias(libs.plugins.kotlinCocoapods)
alias(libs.plugins.kotlinMultiplatform) // Keep
}If all modules have been migrated, also remove the kotlinCocoapods plugin entry from gradle/libs.versions.toml:
[plugins]
# REMOVE: kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }6.2 Remove cocoapods block
Delete the entire cocoapods { ... } block from build.gradle.kts. The swiftPMDependencies {} block and binaries.framework {} configuration added in Phase 3 replace it. Also delete any generated .podspec files from the module directory (e.g., shared/shared.podspec) — these were generated by the CocoaPods plugin and are no longer needed.
6.3 Remove deprecated gradle.properties entries
If found in Phase 1.1, remove from gradle.properties:
# REMOVE — no longer needed after migrating away from CocoaPods (KT-64096)
kotlin.apple.deprecated.allowUsingEmbedAndSignWithCocoaPodsDependencies=true6.4 Clean up CocoaPods-related extras
Review the extras identified in Phase 1 step 12. Podspec metadata, noPodspec(), CocoaPods task hooks, and Pods.xcodeproj patching code are safe to remove without user consultation. Non-standard pod configurations (extraOpts, moduleName), custom cinterop defFile 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
Do NOT stop until the application builds successfully. This phase is iterative — if any step fails, diagnose the error, fix it (consulting troubleshooting.md and re-checking Phases 2–6), and re-run the failing step. Repeat until the build succeeds or the issue is clearly outside the migration scope (pre-existing bug, unrelated tooling problem). Do NOT write the migration report (Phase 8) until the build succeeds.
7.1 Compile Kotlin code
Compile the migrated module to verify Kotlin sources are correct:
./gradlew :moduleName:compileKotlinIosSimulatorArm64If compilation fails with unresolved references, check import transformations (Phase 4) and SwiftPM dependency declarations (Phase 3.2). Common causes: missing importedClangModules, wrong Clang module names, preserved bundled klib imports that should have been transformed (or vice versa).
7.2 Link framework
./gradlew :moduleName:linkDebugFrameworkIosSimulatorArm64If linking fails, check that all required SPM products are declared and that version constraints resolve correctly. Linking errors about missing symbols often indicate a product was omitted from swiftPMDependencies or a version mismatch caused API removal.
7.3 Build iOS/macOS Xcode project
After the Gradle steps succeed, build the Xcode project to verify the full application compiles. Use -project *.xcodeproj if all CocoaPods were removed (Option A), or -workspace *.xcworkspace if non-KMP CocoaPods remain (Option B):
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 buildIf `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. Fix the issue and re-run the failing verification step. Keep iterating until the build succeeds.
---
Phase 8: Migration Report
After the build succeeds, write a comprehensive MIGRATION_REPORT.md in the project root. Use the template in migration-report-template.md.
The report must include: 1. Pre-Migration State — CocoaPods dependencies (name, version, linkOnly), framework config, cocoapods.* imports, non-KMP pods, atypical configuration 2. Migration Steps — exact changes per phase with before/after snippets for non-trivial changes 3. Import Transformations — table of every import change, clearly marking preserved cocoapods.* imports and which bundled klib provides them 4. Errors Encountered — structured Error #N entries: phase, exact symptom, root cause, fix, generalizable flag 5. Non-Trivial Decisions — isStatic changes, preserved imports, framework search paths, trade-offs 6. 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
CocoaPods Extras Patterns
Patterns to look for in build.gradle.kts files beyond the standard cocoapods {} block. These are workarounds, hacks, and glue code that projects accumulate over time to work around CocoaPods limitations.
Detection Patterns (Phase 1 step 11)
- Custom tasks that hook into CocoaPods tasks — e.g., tasks registered with
tasks.named("podInstall") { finalizedBy(...) }ortasks.register("fixXcodeProject")that patchPods.xcodeproj/project.pbxprojto fix paths, tweak build settings, or work around CocoaPods quirks. These are pure CocoaPods workarounds and become dead code after migration. - Pods.xcodeproj patching — any code that reads/writes
Pods.xcodeprojfiles (e.g., replacing Gradle invocation paths, fixing scheme settings). ThePods.xcodeprojwill no longer exist after migration. cocoapods.summary,cocoapods.homepage,cocoapods.version,cocoapods.name— podspec metadata (safe to remove)cocoapods.podfile— explicit Podfile path referencecocoapods.extraSpecAttributes— custom podspec attributespod("...", extraOpts = ...)orpod("...", moduleName = ...)— non-standard pod configurationsnoPodspec()— disables podspec generation- Any code referencing `Pods/` directory, `.xcworkspace`, or `podspec` files — build logic, path constants, or task inputs/outputs tied to CocoaPods artifacts
- Compiler flags or linker settings added specifically for CocoaPods interop (e.g.,
-framework, cinteropdefFilefor pod headers)
---
Phase 6.4 Cleanup Categories
Safe to remove (no user consultation needed)
cocoapods.summary,cocoapods.homepage,cocoapods.version,cocoapods.name— podspec metadata, not used by SPMcocoapods.podfile = project.file(...)— Podfile path reference, not used by SPMcocoapods.extraSpecAttributes— podspec attributes, not used by SPMnoPodspec()— podspec generation flag, not used by SPM- Custom Gradle tasks that hook into CocoaPods tasks (
podInstall,podSetup,generatePodspec) — these tasks no longer exist without the plugin. This includes any tasks registered viatasks.named("podInstall") { finalizedBy(...) }or similar wiring. Example of dead code to remove entirely:
// REMOVE — CocoaPods workaround, no longer needed
tasks.register("fixXcodeProject") {
doLast {
val xcodeProjectFile = project.file("../iosApp/Pods/Pods.xcodeproj/project.pbxproj")
// ... patching Pods.xcodeproj paths ...
}
}
tasks.named("podInstall") { finalizedBy("fixXcodeProject") }- Any code that reads/writes
Pods.xcodeprojfiles — thePods/directory will no longer exist - References to
Pods/directory paths,.xcworkspacefiles, orpodspecfiles in build configurations
Requires analysis — consult the user if unsure
pod("...", extraOpts = ...)— extra options may indicate special compilation flags needed. Check if the underlying library needs equivalent flags inswiftPMDependencies(e.g.,importedClangModules, platform constraints)pod("...", moduleName = ...)— custom module name may indicate the Clang module name differs from the pod name. This likely maps to animportedClangModulesentry in the SPM package declaration- Custom cinterop
defFileconfigurations for pod headers — these may need to be adapted or may no longer be needed if the SwiftPM import handles the headers automatically. Present findings to the user before removing - Compiler or linker flags added specifically for CocoaPods interop (e.g.,
-framework Pod, customcinterops {}blocks) — analyze whether the SPM integration handles this automatically. If unclear, present the flags to the user and ask whether they are still needed - Any custom task wiring or build logic that references CocoaPods outputs — explain what the task does and ask the user whether equivalent functionality is needed
Common Pods to SwiftPM Mapping
Reference for migrating popular CocoaPods dependencies to SwiftPM.
Firebase Suite
All Firebase products come from a single repository: https://github.com/firebase/firebase-ios-sdk.git
Key facts:
- SPM product names match CocoaPods pod names (e.g., pod
FirebaseAuth→ productFirebaseAuth) - Exception: Beta products have a
-Betasuffix in SPM (e.g.,FirebaseAppDistribution-Beta) - The CocoaPods umbrella pod
Firebasedoes not exist in SPM — import specific products - Platform requirements: iOS 15+, macOS 10.15+, tvOS 15+, watchOS 7+
- Xcode: 16.2+
WARNING: Do not mix Firebase across CocoaPods and SPM. All Firebase products share a single repository and common transitive dependencies (gRPC, abseil, leveldb, BoringSSL, nanopb, etc.). If some Firebase pods remain in CocoaPods while others are added via SPM, the shared transitive dependencies get linked twice with conflicting symbols, causing dyld crashes at runtime (e.g.,Symbol not found: _OBJC_CLASS_$_FIRFirestore). When migrating Firebase, move all Firebase pods to SPM at once — including Swift-only pods (FirebaseAI, FirebaseFunctions, FirebaseMLModelDownloader) that Kotlin cannot use directly. Add Swift-only pods asproductsentries withoutimportedClangModules. After adding new products, re-runintegrateLinkagePackageto regenerate the linkage Swift package.
Firebase SPM Products Reference
| CocoaPods Pod | SPM Product | Platform | KMP Notes |
|---|---|---|---|
| FirebaseAnalytics | FirebaseAnalytics | All | ObjC classes: FIRAnalytics, FIRApp |
| FirebaseAuth | FirebaseAuth | All (partial on macOS/tvOS/watchOS) | ObjC classes: FIRAuth, FIRUser |
| FirebaseCore | FirebaseCore | All | ObjC class: FIRApp |
| FirebaseCrashlytics | FirebaseCrashlytics | All | ObjC class: FIRCrashlytics |
| FirebaseDatabase | FirebaseDatabase | All | importedClangModules: `FirebaseDatabaseInternal` — ObjC classes: FIRDatabase, FIRDatabaseReference |
| FirebaseFirestore | FirebaseFirestore | All | Special case — see below |
| FirebaseFunctions | FirebaseFunctions | All | Swift-only — no importedClangModules entry needed |
| FirebaseMessaging | FirebaseMessaging | All | ObjC classes: FIRMessaging |
| FirebaseRemoteConfig | FirebaseRemoteConfig | All | importedClangModules: `FirebaseRemoteConfigInternal` — ObjC class: FIRRemoteConfig |
| FirebaseStorage | FirebaseStorage | All | ObjC class: FIRStorage |
| FirebaseAppCheck | FirebaseAppCheck | All (watchOS 9+) | ObjC class: FIRAppCheck |
| FirebasePerformance | FirebasePerformance | iOS/tvOS only | ObjC class: FIRPerformance |
| FirebaseInAppMessaging | FirebaseInAppMessaging-Beta | iOS/tvOS only | -Beta suffix in SPM, importedClangModules: `FirebaseInAppMessagingInternal` |
| FirebaseAppDistribution | FirebaseAppDistribution-Beta | iOS only | Note -Beta suffix in SPM |
| FirebaseInstallations | FirebaseInstallations | All | ObjC class: FIRInstallations |
| FirebaseABTesting | (no SPM product) | All | Module-only: pulled transitively by RemoteConfig. List in importedClangModules only |
| FirebaseAILogic | FirebaseAI | All | Renamed in SPM. Swift-only — no importedClangModules entry needed |
| FirebaseMLModelDownloader | FirebaseMLModelDownloader | All | Swift-only — no importedClangModules entry needed |
FirebaseAnalytics
// CocoaPods
pod("FirebaseAnalytics") { version = "12.5.0" }
// SwiftPM — use same version as pod
swiftPackage(
url = "https://github.com/firebase/firebase-ios-sdk.git",
version = "12.5.0",
products = listOf("FirebaseAnalytics"),
)Kotlin import:
import swiftPMImport.<group>.<module>.FIRAnalytics
import swiftPMImport.<group>.<module>.FIRAppFirebaseAuth
// CocoaPods
pod("FirebaseAuth") { version = "12.5.0" }
// SwiftPM — use same version as pod
swiftPackage(
url = "https://github.com/firebase/firebase-ios-sdk.git",
version = "12.5.0",
products = listOf("FirebaseAuth"),
)Kotlin import:
import swiftPMImport.<group>.<module>.FIRAuth
import swiftPMImport.<group>.<module>.FIRUserFirebaseDatabase
Database's Clang module name differs from its SPM product name. You must specify importedClangModules (requires typed API):
// CocoaPods
pod("FirebaseDatabase") { version = "12.5.0" }
// SwiftPM - Note the importedClangModules parameter (typed API required)
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.5.0"),
products = listOf(product("FirebaseDatabase")),
importedClangModules = listOf("FirebaseDatabaseInternal"),
)Kotlin import:
import swiftPMImport.<group>.<module>.FIRDatabase
import swiftPMImport.<group>.<module>.FIRDatabaseReferenceFirebaseFirestore (Special Case)
Firestore's Clang module name differs from its SPM product name. You must specify importedClangModules (requires typed API):
// CocoaPods
pod("FirebaseFirestore") { version = "12.5.0" }
// SwiftPM - Note the importedClangModules parameter (typed API required)
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.5.0"),
products = listOf(product("FirebaseFirestore")),
importedClangModules = listOf("FirebaseFirestoreInternal"),
)Kotlin import:
import swiftPMImport.<group>.<module>.FIRFirestore
import swiftPMImport.<group>.<module>.FIRDocumentReferenceWhy is this needed? Firestore distributes as a binary xcframework. The internal Clang module exposed to Objective-C is named FirebaseFirestoreInternal, not FirebaseFirestore. Without importedClangModules, the KMP compiler cannot discover the Objective-C headers.
FirebaseCrashlytics
// CocoaPods
pod("FirebaseCrashlytics") { version = "12.5.0" }
// SwiftPM — use same version as pod
swiftPackage(
url = "https://github.com/firebase/firebase-ios-sdk.git",
version = "12.5.0",
products = listOf("FirebaseCrashlytics"),
)iOS project requirement: Crashlytics needs a dSYM upload run script in the Xcode build phases. After migration, add a "Run Script" phase at the END of build phases:
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"With input files:
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)Also set Debug Information Format to DWARF with dSYM File for all build configurations.
Combined Firebase Example
When using multiple Firebase products, declare them in a single package. Set `discoverClangModulesImplicitly = false` — Firebase's transitive C++ dependencies (gRPC, abseil, leveldb, BoringSSL) contain Clang modules that fail cinterop. Explicitly list only the modules you need.
swiftPMDependencies {
discoverClangModulesImplicitly = false
// Combined Firebase requires typed API for importedClangModules control
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.5.0"),
products = listOf(
product("FirebaseAnalytics"),
product("FirebaseAuth"),
product("FirebaseDatabase"),
product("FirebaseFirestore"),
product("FirebaseCrashlytics"),
product("FirebaseMessaging"),
product("FirebaseRemoteConfig"),
// Swift-only pods (products only, no importedClangModules):
product("FirebaseAI"),
product("FirebaseFunctions"),
),
importedClangModules = listOf(
"FirebaseAnalytics",
"FirebaseAuth",
"FirebaseCore",
"FirebaseCrashlytics",
"FirebaseDatabaseInternal", // Not "FirebaseDatabase"
"FirebaseFirestoreInternal", // Not "FirebaseFirestore"
"FirebaseMessaging",
"FirebaseRemoteConfigInternal", // Not "FirebaseRemoteConfig"
"FirebaseABTesting", // Module-only, no product
),
)
}Firebase importedClangModules Reference
Several Firebase products expose ObjC headers through Clang modules whose names differ from the SPM product name:
| SPM Product | Clang Module (importedClangModules) | Notes |
|---|---|---|
| FirebaseAnalytics | FirebaseAnalytics | Same name |
| FirebaseAuth | FirebaseAuth | Same name |
| FirebaseCore | FirebaseCore | Same name |
| FirebaseCrashlytics | FirebaseCrashlytics | Same name |
| FirebaseDatabase | FirebaseDatabaseInternal | Different |
| FirebaseFirestore | FirebaseFirestoreInternal | Different |
| FirebaseInAppMessaging-Beta | FirebaseInAppMessagingInternal | Different |
| FirebaseRemoteConfig | FirebaseRemoteConfigInternal | Different |
| FirebaseInstallations | FirebaseInstallations | Same name |
| FirebaseMessaging | FirebaseMessaging | Same name |
| FirebasePerformance | FirebasePerformance | Same name |
| FirebaseStorage | FirebaseStorage | Same name |
| FirebaseAppCheck | FirebaseAppCheck | Same name |
| FirebaseAppDistribution-Beta | FirebaseAppDistribution | Same name (no -Beta) |
| (transitive) | FirebaseABTesting | Module-only, no product |
| FirebaseAI | (none) | Swift-only, no cinterop |
| FirebaseFunctions | (none) | Swift-only, no cinterop |
| FirebaseMLModelDownloader | (none) | Swift-only, no cinterop |
Note: When discoverClangModulesImplicitly = false (recommended for Firebase), you must list every Clang module you import in importedClangModules. When true (default), importedClangModules is ignored — but this will fail for Firebase due to C++ transitive dependencies.
Firebase Initialization
Ensure GoogleService-Info.plist is included in the iOS app target. In the app's entry point:
import Firebase
FirebaseApp.configure() // Must be called before using any Firebase service---
Google Maps
Repository: https://github.com/googlemaps/ios-maps-sdk.git
Key facts:
- iOS 16+ only — no macOS, tvOS, or watchOS support
- Xcode 16.0+ required
- Must use
exact()version —from()will fail to resolve - Single SPM product:
GoogleMaps(wraps a binary xcframework viaGoogleMapsTarget) - CocoaPods subspec
GoogleMaps/Mapsmaps to the singleGoogleMapsSPM product - Requires a Google Maps Platform API key configured in the iOS app
- Check releases for available SPM versions
// CocoaPods
pod("GoogleMaps") { version = "10.10.0" }
// SwiftPM — use the exact same version as the pod
swiftPackage(
url = "https://github.com/googlemaps/ios-maps-sdk.git",
version = "10.10.0",
products = listOf("GoogleMaps"),
)Kotlin import:
import swiftPMImport.<group>.<module>.GMSMapView
import swiftPMImport.<group>.<module>.GMSCameraPosition
import swiftPMImport.<group>.<module>.GMSMarker
import swiftPMImport.<group>.<module>.GMSServicesiOS project requirement: The API key must be set in the app delegate or SwiftUI app entry point:
import GoogleMaps
GMSServices.provideAPIKey("YOUR_API_KEY")---
Google Sign-In
Repository: https://github.com/google/GoogleSignIn-iOS.git
Key facts:
- iOS 12+, macOS 10.15+ — broad platform support
- Two SPM products:
GoogleSignIn(core) andGoogleSignInSwift(SwiftUI support) - CocoaPods pods:
GoogleSignInandGoogleSignInSwiftSupport - Uses
from()versioning (latest: 9.1.0)
// CocoaPods
pod("GoogleSignIn") { version = "8.0.0" }
// SwiftPM — use same version as pod
swiftPackage(
url = "https://github.com/google/GoogleSignIn-iOS.git",
version = "8.0.0",
products = listOf("GoogleSignIn"),
)Kotlin import:
import swiftPMImport.<group>.<module>.GIDSignIn
import swiftPMImport.<group>.<module>.GIDSignInButtoniOS project requirement: Add GIDClientID to Info.plist and configure the URL scheme for OAuth redirect. See Google Sign-In iOS docs.
---
LoremIpsum
Simple text generation library with direct mapping.
// CocoaPods
pod("LoremIpsum") { version = "2.0.1" }
// SwiftPM — use same version as pod
swiftPackage(
url = "https://github.com/lukaskubanek/LoremIpsum.git",
version = "2.0.1",
products = listOf("LoremIpsum"),
)Kotlin import:
import swiftPMImport.<group>.<module>.LoremIpsum---
Quick Reference Table
| Pod Name | SPM Product | SPM Repository | Version Type | Platform | Notes |
|---|---|---|---|---|---|
| FirebaseAnalytics | FirebaseAnalytics | firebase/firebase-ios-sdk.git | from() | All | |
| FirebaseAuth | FirebaseAuth | firebase/firebase-ios-sdk.git | from() | All | |
| FirebaseCore | FirebaseCore | firebase/firebase-ios-sdk.git | from() | All | |
| FirebaseCrashlytics | FirebaseCrashlytics | firebase/firebase-ios-sdk.git | from() | All | Needs dSYM upload script |
| FirebaseDatabase | FirebaseDatabase | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseDatabaseInternal |
| FirebaseFirestore | FirebaseFirestore | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseFirestoreInternal |
| FirebaseFunctions | FirebaseFunctions | firebase/firebase-ios-sdk.git | from() | All | Swift-only, no cinterop |
| FirebaseMessaging | FirebaseMessaging | firebase/firebase-ios-sdk.git | from() | All | |
| FirebaseRemoteConfig | FirebaseRemoteConfig | firebase/firebase-ios-sdk.git | from() | All | importedClangModules: FirebaseRemoteConfigInternal |
| FirebaseStorage | FirebaseStorage | firebase/firebase-ios-sdk.git | from() | All | |
| FirebasePerformance | FirebasePerformance | firebase/firebase-ios-sdk.git | from() | iOS/tvOS | |
| FirebaseInAppMessaging | FirebaseInAppMessaging-Beta | firebase/firebase-ios-sdk.git | from() | iOS/tvOS | -Beta suffix, importedClangModules: FirebaseInAppMessagingInternal |
| FirebaseAppDistribution | FirebaseAppDistribution-Beta | firebase/firebase-ios-sdk.git | from() | iOS only | -Beta suffix |
| FirebaseABTesting | (none) | firebase/firebase-ios-sdk.git | — | All | Module-only, importedClangModules only |
| FirebaseAILogic | FirebaseAI | firebase/firebase-ios-sdk.git | from() | All | Renamed, Swift-only |
| GoogleMaps | GoogleMaps | googlemaps/ios-maps-sdk.git | exact() | iOS 16+ only | |
| GoogleSignIn | GoogleSignIn | google/GoogleSignIn-iOS.git | from() | iOS 12+, macOS 10.15+ | |
| GoogleSignInSwiftSupport | GoogleSignInSwift | google/GoogleSignIn-iOS.git | from() | iOS 12+, macOS 10.15+ | SwiftUI support |
| LoremIpsum | LoremIpsum | lukaskubanek/LoremIpsum.git | from() | All |
---
KMP Wrapper Libraries with Bundled Cinterop Klibs
Some KMP libraries that wrap iOS SDKs ship pre-built cinterop klibs using the cocoapods.* package namespace. After migrating to SwiftPM, these cocoapods.* imports must be preserved — they resolve to the library's bundled klib, not to actual CocoaPods infrastructure.
KMPNotifier
Repository: https://github.com/mirzemehdi/KMPNotifier Maven: io.github.mirzemehdi:kmpnotifier
What it provides: A KMP push notification library that wraps Firebase Cloud Messaging on iOS. The library bundles its own cinterop klib with namespace cocoapods.FirebaseMessaging, providing Kotlin bindings for FIRMessaging, FIRMessagingAPNSTokenType, and related classes.
Impact on migration:
- When
swiftPMDependenciesgenerates cinterop bindings, it detects thatFirebaseMessagingbindings already exist in KMPNotifier's klib and skips generating new bindings for that Clang module import cocoapods.FirebaseMessaging.FIRMessagingmust remain unchanged — do NOT replace withswiftPMImport.*FirebaseMessagingshould still be listed inproductsandimportedClangModulesfor SPM linking, even though cinterop bindings won't be generated for it
Verifying bundled klib contents: Use klib dump-metadata-signatures to inspect what a library's klib provides (docs):
find ~/.gradle/caches -name "*.klib" -path "*kmpnotifier*" | head -1
klib dump-metadata-signatures /path/to/cinterop.klib | grep "FIRMessaging"
# Shows: cocoapods.FirebaseMessaging/FIRMessaging → confirms bundled klibExample — project using both KMPNotifier and GoogleSignIn:
// IOSDelegate.kt — after migration
import cocoapods.FirebaseMessaging.FIRMessaging // KEEP — from kmpnotifier klib
import cocoapods.FirebaseMessaging.FIRMessagingAPNSTokenType // KEEP — from kmpnotifier klib
import swiftPMImport.com.example.app.GIDSignIn // REPLACE — direct cinteropdev.gitlive/firebase-kotlin-sdk
Repository: https://github.com/GitLiveApp/firebase-kotlin-sdk Maven: dev.gitlive:firebase-auth, dev.gitlive:firebase-firestore, dev.gitlive:firebase-storage, etc.
What it provides: Kotlin-first Firebase APIs for KMP. Unlike KMPNotifier, dev.gitlive libraries provide high-level Kotlin APIs — you typically don't use cocoapods.* imports directly. Instead, the Firebase pods were declared with linkOnly = true in CocoaPods to provide native linking only.
Impact on migration:
1. Linker flags baked into published klibs. The dev.gitlive klibs contain -framework FirebaseCore, -framework FirebaseAuth, etc. from the CocoaPods era. These persist when the consuming project switches to SPM. With SPM, Firebase frameworks land in per-product subdirectories ($BUILT_PRODUCTS_DIR/FirebaseCore/FirebaseCore.framework) that the K/N linker doesn't search automatically.
Fix: Add per-product -F linkerOpts to build.gradle.kts:
val builtProductsDir = System.getenv("BUILT_PRODUCTS_DIR")
if (builtProductsDir != null) {
listOf("FirebaseCore", "FirebaseAuth", "FirebaseCoreExtension",
"FirebaseCoreInternal", "FirebaseCrashlytics", "FirebaseFirestore",
"FirebaseFirestoreInternal", "FirebaseInstallations", "FirebaseMessaging",
"FirebaseStorage", "GoogleDataTransport", "GoogleUtilities",
"GTMSessionFetcher", "AppCheckCore", /* ... */).forEach { product ->
linkerOpts("-F", "$builtProductsDir/$product")
}
}Also add matching FRAMEWORK_SEARCH_PATHS in the Xcode project for both Debug and Release.
2. Must use `isStatic = true`. With a dynamic framework, the K/N linker creates @rpath/FirebaseCore.framework/FirebaseCore load instructions. Firebase SPM products are static libraries — their .framework bundles are not embedded in the app bundle. At runtime, dyld crashes with Library not loaded. Switching to isStatic = true embeds all symbols and defers unresolved framework flags to the final Xcode link.
3. iOS test tasks may fail. The K/N test runner cannot find Firebase frameworks outside of Xcode context. You may need to disable iOS test tasks:
tasks.matching {
(it.name.contains("Ios") || it.name.contains("ios")) &&
(it.name.contains("Test") || it.name.contains("test"))
}.configureEach { enabled = false }---
Researching Other Pods
For pods not listed here:
1. Check GitHub repository - Look for a Package.swift file in the repo 2. Check CocoaPods spec - The source field often points to the Git URL 3. Search Swift Package Index - https://swiftpackageindex.com/ 4. Check library documentation - Many libraries document SPM installation
Finding the Clang Module Name
If you're unsure of the correct Clang module name:
1. Keep discoverClangModulesImplicitly = true (default) 2. Run ./gradlew build 3. Check build errors for available class names 4. Or check the library's module.modulemap file in its source
Identifying Bundled Cinterop Klibs in Unknown Libraries
If you suspect a KMP library bundles its own cinterop klibs (common for libraries wrapping iOS SDKs), use the klib tool to inspect them (docs):
# Find klibs from a specific library in Gradle caches
find ~/.gradle/caches -name "*.klib" -path "*libraryName*"
# Dump API signatures to see what namespaces and classes are provided
klib dump-metadata-signatures /path/to/library.klib | grep "cocoapods\."
# If output shows cocoapods.* entries, the library bundles cinterop klibs
# Those cocoapods.* imports must be preserved after migrationIndicators that a library may bundle cinterop klibs:
- The project has
linkOnly = truepod declarations for the same native SDK - The library's documentation mentions CocoaPods integration or cinterop
- The library provides Kotlin APIs for an iOS SDK (Firebase, Maps, etc.)
Version Compatibility
Do NOT bump dependency versions during migration — use the exact same version from the cocoapods {} block. Always: 1. Use the same version. If the pod was version = "1.4.1", the SPM package must be exact("1.4.1"). Bumping versions can break cinterop APIs and introduce issues unrelated to the migration. 2. CocoaPods version = "X.Y.Z" (without ~>) is an exact pin — use exact("X.Y.Z") in SPM, not from(). Only use from() when the CocoaPods spec used optimistic versioning (~>). 3. Check the GitHub releases page to confirm the exact version is available as an SPM release 4. Test thoroughly after migration
SwiftPM Import DSL Reference
Complete reference for the swiftPMDependencies {} DSL in Kotlin Multiplatform.
Basic Structure
swiftPackage() and localSwiftPackage() are annotated with @ExperimentalKotlinGradlePluginApi (warning level). Add the opt-in at the top of build.gradle.kts:
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)kotlin {
iosArm64()
iosSimulatorArm64()
swiftPMDependencies {
// Deployment versions
iosMinimumDeploymentTarget = "16.0"
macosMinimumDeploymentTarget = "13.0"
tvosMinimumDeploymentTarget = "16.0"
watchosMinimumDeploymentTarget = "9.0"
// Module discovery (default: true)
discoverClangModulesImplicitly = true
// Package declarations
swiftPackage(...)
localSwiftPackage(...)
}
}---
Package Declaration
The DSL has two API forms. Use the simple string API for most packages. Use the typed API only when you need exact(), branch(), revision(), or platform constraints.
Simple API (Preferred)
Plain strings for URL, version, and products. The version parameter maps to a minimum version (from()) internally. The importedClangModules defaults to the products list automatically.
swiftPackage(
url = "https://github.com/owner/repo.git",
version = "1.0.0", // Equivalent to from("1.0.0") — minimum version
products = listOf("ProductName", "AnotherProduct"),
)Typed API (Advanced)
Use when you need exact version pinning, branch tracking, platform constraints, or explicit Clang module control:
swiftPackage(
url = url("https://github.com/owner/repo.git"),
version = exact("1.0.0"),
products = listOf(
product("ProductName"),
product("PlatformSpecific", platforms = setOf(iOS()))
),
importedClangModules = listOf("CustomClangModuleName"),
)Remote Package (Swift Package Registry)
swiftPackage(
repository = id("scope.package-name"),
version = from("1.0.0"),
products = listOf(product("ProductName")),
packageName = "package-name",
)Local Package
localSwiftPackage(
directory = layout.projectDirectory.dir("../LocalPackage"),
products = listOf("LocalPackage"),
)To create a new local package (e.g., a Swift/ObjC wrapper around a Swift-only library):
cd /path/to/shared
mkdir LocalPackage && cd LocalPackage
swift package init --type library --name LocalPackageThen use it in Kotlin:
// src/appleMain/kotlin/useLocalPackage.kt
import swiftPMImport.<group>.<module>.HelloFromLocalPackage
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
fun useLocalPackage() {
HelloFromLocalPackage().hello()
}---
Version Specification
| Syntax | Description | Use Case |
|---|---|---|
version = "1.0.0" (simple API) | Minimum version — equivalent to from("1.0.0") | Most packages |
version = exact("1.0") | Exact version pin | Strict dependencies, migration |
version = from("1.0") | Minimum version (explicit) | Same as simple string |
version = branch("name") | Git branch | Development, testing |
version = revision("hash") | Git commit hash | Pinning specific commits |
version = range("1.0", "2.0") | Version range | Constraining upper bound |
Important for migration: The simple string version = "X.Y.Z" resolves to a minimum version (from()), which may pull a newer version than what was in CocoaPods. For exact version preservation during migration, use the typed API: version = exact("X.Y.Z").
---
Product Configuration
Simple API
products = listOf("FirebaseAnalytics", "FirebaseAuth")With the simple API, importedClangModules defaults to the same list as products. This works when product names match Clang module names.
Typed API — Platform Constraints
For packages that only support certain platforms, use the typed product() function:
products = listOf(
product("GoogleMaps", platforms = setOf(iOS())) // iOS only
)Available platforms: iOS(), macOS(), tvOS(), watchOS()
Typed API — Per-Product Clang Module Override
products = listOf(
product("FirebaseDatabase", importedClangModules = setOf("FirebaseDatabaseInternal"))
)---
Module Import Configuration
Automatic Discovery (Default)
By default, discoverClangModulesImplicitly = true. SwiftPM import automatically discovers and imports all accessible Clang modules.
IMPORTANT: When discoverClangModulesImplicitly = true, the importedClangModules parameter is ignored. Only set importedClangModules when discoverClangModulesImplicitly = false.
IMPORTANT for Firebase: Set discoverClangModulesImplicitly = false when using Firebase. Firebase's transitive C++ dependencies (gRPC, abseil, leveldb, BoringSSL) contain Clang modules that fail cinterop generation. Disable implicit discovery and explicitly list only the Firebase modules you need in importedClangModules.
Explicit Module Import
When automatic discovery is disabled and the Clang module name differs from the product name, use the typed API:
swiftPMDependencies {
discoverClangModulesImplicitly = false // Disable auto-discovery
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.6.0"),
products = listOf(
product("FirebaseAnalytics"),
product("FirebaseFirestore")
),
importedClangModules = listOf(
"FirebaseAnalytics",
"FirebaseCore",
"FirebaseFirestoreInternal" // Note: different from product name
),
)
}When to Use importedClangModules
| Scenario | Use importedClangModules? |
|---|---|
| Simple API, product name = Clang module name | No (auto-defaulted from products) |
| Product name != Clang module name | Yes (typed API) |
| Multiple modules per product | Yes (typed API) |
| Using discoverClangModulesImplicitly = false | Yes (typed API) |
---
Deployment Versions
Set minimum deployment targets for each platform:
swiftPMDependencies {
iosMinimumDeploymentTarget = "16.0"
macosMinimumDeploymentTarget = "13.0"
tvosMinimumDeploymentTarget = "16.0"
watchosMinimumDeploymentTarget = "9.0"
}---
Complete Example
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
group = "org.example.myproject"
version = "1.0-SNAPSHOT"
kotlin {
iosArm64()
iosSimulatorArm64()
// Framework configuration (moved from cocoapods block)
listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "SharedModule"
isStatic = true
}
}
swiftPMDependencies {
iosMinimumDeploymentTarget = "16.0"
// Simple API — most packages
swiftPackage(
url = "https://github.com/lukaskubanek/LoremIpsum.git",
version = "2.0.1",
products = listOf("LoremIpsum"),
)
// Simple API — Google Maps
swiftPackage(
url = "https://github.com/googlemaps/ios-maps-sdk.git",
version = "10.3.0",
products = listOf("GoogleMaps"),
)
// Local package
localSwiftPackage(
directory = layout.projectDirectory.dir("LocalWrapper"),
products = listOf("LocalWrapper"),
)
}
compilerOptions {
optIn.add("kotlinx.cinterop.ExperimentalForeignApi")
}
}---
Transitive Dependencies
SwiftPM dependencies are handled automatically. When you run Kotlin/Native tests or link a framework, the Kotlin Gradle Plugin will provision necessary machine code from transitive SwiftPM dependencies. This behavior is automatic.
You can optionally declare transitive dependencies explicitly to pin specific versions:
swiftPMDependencies {
// Main dependency
swiftPackage(
url = "https://github.com/firebase/firebase-ios-sdk.git",
version = "12.5.0",
products = listOf("FirebaseAnalytics"),
)
// Transitive dependency with explicit version pin
swiftPackage(
url = url("https://github.com/apple/swift-protobuf.git"),
version = exact("1.32.0"),
products = listOf(product("SwiftProtobuf")),
)
}Migration Report Template
After migration (whether successful or not), write a comprehensive MIGRATION_REPORT.md in the project root. This document serves both as human-readable documentation and as structured input for AI agents analyzing the migration.
Template
# Migration Report: CocoaPods to SwiftPM Import
**Project:** <project name>
**Module migrated:** <module name>
**Date:** <YYYY-MM-DD>
**Kotlin version:** <old version> → <new version>
**Status:** <Completed successfully | Completed with workarounds | Failed — see Errors>
---
## Pre-Migration State
### CocoaPods Dependencies
| Pod | Version | Mode | Notes |
|-----|---------|------|-------|
| <PodName> | <version> | Regular / linkOnly | <e.g., cinterop used in Kotlin code> |
### Framework Configuration
- **baseName:** <name>
- **isStatic:** <true/false> → <true/false after migration>
- **Deployment target:** <version>
### Kotlin Files Using `cocoapods.*` Imports
| File | Imports |
|------|---------|
| <path> | `cocoapods.<Module>.<Class>`, ... |
### Non-KMP CocoaPods
<List any pods in Podfile not managed by KMP, or "None">
### Atypical Project Configuration
<Document anything unusual found in Phase 1 that required special handling:
EmbedAndSign disablers, commented-out build phases, custom Gradle tasks
hooking into CocoaPods, non-standard framework configs, missing `group`
property, etc. If nothing unusual, write "Standard configuration.">
---
## Migration Steps
### Phase 2: Gradle Configuration
<List exact changes made to settings.gradle.kts, libs.versions.toml,
root build.gradle.kts, gradle.properties. Include before/after snippets
for non-trivial changes.>
### Phase 3: swiftPMDependencies
<Show the complete `swiftPMDependencies {}` block added.
Document decisions: why `discoverClangModulesImplicitly = false`,
which `importedClangModules` were chosen and why, framework search
paths added, static/dynamic choice, etc.>
### Phase 4: Import Transformations
<Table of import changes. Clearly mark any preserved `cocoapods.*` imports.>
| File | Before | After | Source |
|------|--------|-------|--------|
| <path> | `cocoapods.<Mod>.<Cls>` | `swiftPMImport.<grp>.<mod>.<Cls>` | swiftPMImport cinterop |
| <path> | `cocoapods.<Mod>.<Cls>` | `cocoapods.<Mod>.<Cls>` (unchanged) | <library> bundled klib |
### Phase 5: iOS Project Reconfiguration
<Document: Option A or B, integration commands run, sandboxing fix,
Crashlytics dSYM script update, any manual pbxproj edits.>
### Phase 6: CocoaPods Removal
<List everything removed: plugin, cocoapods block, gradle.properties
entries, custom tasks, podspec files, Podfile changes.>
### Phase 7: Verification
<Build commands run and their outcomes. Include the final successful
build command or note that verification was deferred to the user.>
---
## Errors Encountered
<For each error, use this structure:>
### Error #N: <Short title>
**Phase:** <which phase>
**Symptom:** <exact error message or behavior>
**Root cause:** <why it happened>
**Fix:** <what was done to resolve it>
**Generalizable:** <Yes/No — is this likely to affect other projects?>
---
## Non-Trivial Decisions
<Document decisions that required judgment, not just following the guide:
- Why a specific `importedClangModules` list was chosen
- Why `isStatic` was changed (or kept)
- Why certain `cocoapods.*` imports were preserved
- Framework search paths added and how the product list was determined
- Any trade-offs made (e.g., disabling iOS tests)>
---
## Files Changed
<Complete list of files modified, created, or deleted during migration.
Group by type: Gradle files, Kotlin sources, Xcode project files, other.>
### Gradle Files
- <path> — <brief description of change>
### Kotlin Sources
- <path> — <brief description of change>
### Xcode Project Files
- <path> — <brief description of change>
### Created
- <path> — <what it is>
### Deleted
- <path> — <what it was>Writing Guidelines
- Be specific. Include actual file paths, class names, error messages. Avoid vague statements like "updated the config."
- Show before/after. For non-trivial changes, include code snippets of what was changed and why.
- Explain the "why." Every error and non-trivial decision should include root cause analysis, not just the fix.
- *Mark preserved `cocoapods.` imports clearly.** These are the most confusing aspect of the migration for future readers — explain exactly why each one was kept and which library provides the bundled klib.
- Flag generalizable issues. Mark errors that are likely to affect other projects so this report can improve the migration tooling.
- Keep it machine-parseable. Use consistent markdown headings, tables, and the
Error #Nformat so AI agents can extract structured data.
Troubleshooting Guide
Common issues and solutions when migrating from CocoaPods to SwiftPM.
Gradle Issues
Import Not Found After Migration
Symptom: Unresolved reference errors for classes that worked with CocoaPods
Solution: The import namespace follows a specific pattern:
swiftPMImport.<group>.<module>.<ClassName>Steps to fix: 1. Check group property in build.gradle.kts 2. Replace - with . in both group and module names 3. Run ./gradlew build to see available classes in error messages
Example:
// If group = "org.jetbrains.kotlin.firebase-sample" and module = "kotlin-library"
// Import becomes:
import swiftPMImport.org.jetbrains.kotlin.firebase.sample.kotlin.library.FIRAnalytics
// ^ ^ ^
// dashes become dots --------+------+---
Gradle Sync Fails
Symptom: IDE fails to sync project after adding swiftPMDependencies
Solution: 1. Invalidate caches: File > Invalidate Caches > Invalidate and Restart 2. Run ./gradlew --refresh-dependencies 3. Check all repository declarations include JetBrains Maven
---
Linker Issues
Missing Symbols / Linker Errors
Symptom: Undefined symbols for architecture errors
Solutions:
1. Run the linkage integration task (one-time, not a build phase):
./gradlew :moduleName:integrateLinkagePackage2. Verify SPM package is linked in Xcode:
- Open project in Xcode
- Check Package Dependencies section
- Ensure
KotlinMultiplatformLinkedPackageis present
3. Check framework configuration — isStatic = true is recommended. While isStatic = false can work, dynamic frameworks have known edge cases with SwiftPM import (linker errors, dyld crashes, duplicate class warnings). It is required with dev.gitlive:firebase-* — see below.
---
"No such module" in Xcode
Symptom: Xcode can't find the Kotlin module
Solution: 1. Clean Xcode build folder: Shift+Cmd+K 2. Re-run integration:
./gradlew :moduleName:integrateLinkagePackage3. Restart Xcode completely 4. Re-open the correct Xcode project file (.xcodeproj if all CocoaPods were removed, .xcworkspace if non-KMP CocoaPods remain)
---
Build Phase Issues
Build Phase Order Problems
Symptom: Swift compilation fails because Kotlin framework isn't ready
Solution: Ensure "Compile Kotlin" runs BEFORE "Compile Sources":
1. Open Xcode project 2. Select app target > Build Phases 3. Drag "Compile Kotlin" phase above "Compile Sources"
---
Script Sandboxing Errors
Symptom: Gradle task checkSandboxAndWriteProtection fails during Xcode build:
Execution failed for task ':moduleName:checkSandboxAndWriteProtection'.
> User Script Sandboxing Enabled in Xcode ProjectOr build scripts can't access files or run Gradle.
Cause: Xcode 16+ enables User Script Sandboxing by default. The Gradle build phase needs to write to the project directory, which sandboxing prevents.
Solution:
1. Disable via command line:
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' /path/to/iosApp/*.xcodeproj/project.pbxprojIf the setting is not present in the .pbxproj (Xcode defaults to YES without an explicit entry), open the project in Xcode instead.
2. Or disable in Xcode: select app target → Build Settings → Build Options → set "User Script Sandboxing" to NO
3. Important: After changing the setting, stop the Gradle daemon:
./gradlew --stop---
Integration Task Issues
integrateEmbedAndSign Skipped or Does Nothing
Symptom: Running integrateEmbedAndSign completes without errors but the Xcode project is not modified. The embedAndSignAppleFrameworkForXcode build phase is not added or remains commented out.
Cause: The project has code that disables EmbedAndSign tasks. Common patterns:
// In root or module build.gradle.kts
project.gradle.taskGraph.whenReady {
allTasks.filter { it::class.simpleName?.contains("EmbedAndSign") == true }.forEach {
it.enabled = false
}
}This was a CocoaPods-era workaround that inadvertently disables integrateEmbedAndSign.
Solution: Remove the disabler code from build.gradle.kts, then re-run the integration command.
---
embedAndSignAppleFrameworkForXcode Commented Out in Build Phase
Symptom: Xcode build succeeds but produces no Kotlin framework. The app crashes at runtime with missing module errors.
Cause: The Gradle invocation in the Xcode build phase script was commented out (prefixed with #) — possibly a pre-existing state from before migration.
Solution: Open project.pbxproj and uncomment the Gradle invocation:
-#./gradlew :moduleName:embedAndSignAppleFrameworkForXcode
+./gradlew :moduleName:embedAndSignAppleFrameworkForXcode---
Third-Party KMP Libraries with Bundled Klibs
cocoapods.* Class Not Found After Converting to swiftPMImport.*
Symptom: After replacing import cocoapods.FirebaseMessaging.FIRMessaging with import swiftPMImport.<group>.<module>.FIRMessaging, the build fails with Unresolved reference 'FIRMessaging'. Other swiftPMImport classes (e.g., GIDSignIn) resolve fine.
Cause: A third-party KMP library (e.g., KMPNotifier — io.github.mirzemehdi:kmpnotifier) bundles its own pre-built cinterop klib with the cocoapods.FirebaseMessaging namespace. The swiftPMDependencies cinterop generator detects these existing bindings and deliberately skips generating new bindings for that Clang module to avoid duplicate symbols. The swiftPMImport.* bindings for that module simply don't exist.
Solution: Revert the affected imports back to cocoapods.*:
// These resolve to the third-party library's bundled klib, NOT actual CocoaPods
import cocoapods.FirebaseMessaging.FIRMessaging
import cocoapods.FirebaseMessaging.FIRMessagingAPNSTokenTypeThe cocoapods prefix here is just a package namespace embedded in the library's published artifact — no CocoaPods infrastructure is needed at runtime.
How to identify bundled klibs in advance: Check if the project depends on KMP libraries that wrap iOS SDKs. Known libraries: KMPNotifier (bundles cocoapods.FirebaseMessaging). Also check for linkOnly = true pod declarations — this indicates the pod was only needed for linking while a KMP library provided the actual bindings.
Inspecting klib contents: Use klib dump-metadata-signatures to verify which classes a klib provides (docs):
# Find the klib
find ~/.gradle/caches -name "*.klib" -path "*kmpnotifier*" | head -1
# Dump and search for the class in question
klib dump-metadata-signatures /path/to/cinterop.klib | grep "FIRMessaging"
# Output shows: cocoapods.FirebaseMessaging.FIRMessaging → confirms bundled klibYou can also compare before/after migration by dumping the swiftPMImport klib:
# After build, find the swiftPMImport klib
find . -name "*.klib" -path "*swiftPMImport*" | head -1
# Verify which classes are available
klib dump-metadata-signatures /path/to/swiftPMImport.klib | grep "FIRMessaging"
# Empty output = class NOT in swiftPMImport (must use cocoapods.* import)---
dev.gitlive/firebase-kotlin-sdk Issues
framework 'FirebaseCore' not found (K/N Linker)
Symptom: Kotlin/Native linker fails with:
ld: framework 'FirebaseCore' not foundor similar errors for FirebaseAuth, FirebaseFirestore, etc. The Gradle compilation succeeds but the link step fails.
Cause: firebase-kotlin-sdk (dev.gitlive:firebase-*) was published with CocoaPods-era cinterop klibs. These klibs have -framework FirebaseCore, -framework FirebaseAuth, etc. baked into their linker metadata. With CocoaPods, those frameworks were in Pods/ on the search path. With SPM, they land in per-product subdirectories ($BUILT_PRODUCTS_DIR/FirebaseCore/FirebaseCore.framework) that the K/N linker doesn't search.
Solution (two-part):
Part A — Gradle linkerOpts:
iosTarget.binaries.framework {
val builtProductsDir = System.getenv("BUILT_PRODUCTS_DIR")
if (builtProductsDir != null) {
listOf(
"FirebaseCore", "FirebaseAuth", "FirebaseCoreExtension",
"FirebaseCoreInternal", "FirebaseCrashlytics", "FirebaseFirestore",
"FirebaseFirestoreInternal", "FirebaseInstallations", "FirebaseMessaging",
"FirebaseStorage", "GoogleDataTransport", "GoogleUtilities",
"GTMSessionFetcher", "AppCheckCore", "AppAuth", "GTMAppAuth",
).forEach { product ->
linkerOpts("-F", "$builtProductsDir/$product")
}
}
}The if (builtProductsDir != null) guard ensures ./gradlew :moduleName:compileKotlinIosSimulatorArm64 works without Xcode (compilation doesn't link).
Part B — Xcode FRAMEWORK_SEARCH_PATHS:
Add matching entries in project.pbxproj for both Debug and Release buildSettings:
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(BUILT_PRODUCTS_DIR)/FirebaseCore",
"$(BUILT_PRODUCTS_DIR)/FirebaseAuth",
"$(BUILT_PRODUCTS_DIR)/FirebaseCoreExtension",
// ... same list as Part A ...
);---
dyld: Library not loaded: @rpath/FirebaseCore.framework/FirebaseCore (Runtime Crash)
Symptom: The Gradle build and Xcode compilation both succeed, but the app crashes at launch with:
dyld: Library not loaded: @rpath/FirebaseCore.framework/FirebaseCore
Referenced from: .../ComposeApp.framework/ComposeAppCause: The KMP framework is dynamic (isStatic = false or default). The K/N linker creates LC_LOAD_DYLIB entries (@rpath/FirebaseCore.framework/FirebaseCore). Firebase SPM products are static libraries — their .framework bundles exist in $BUILT_PRODUCTS_DIR during build but are NOT embedded in the app bundle. At runtime, dyld searches @rpath and finds nothing.
Solution: Switch to a static framework:
iosTarget.binaries.framework {
baseName = "Shared"
isStatic = true // Required when using dev.gitlive:firebase-* with SPM
}With a static framework, all symbols are embedded in the .a archive. No LC_LOAD_DYLIB entries are created. Unresolved -framework flags from dev.gitlive klibs are deferred to the final Xcode app link, where KotlinMultiplatformLinkedPackage provides them.
After switching to static, also: 1. Re-run integrateLinkagePackage — regenerates Package.swift with type: .none (static) 2. Remove any "Embed Frameworks" copy phase for the KMP framework — static frameworks must NOT be embedded 3. Add linker flags previously resolved by the K/N linker (e.g., -framework Accelerate, -weak_framework CoreML) to OTHER_LDFLAGS in the Xcode project
---
Firebase-Specific Issues
cinterop Failures on C++ Modules (gRPC, abseil, leveldb, BoringSSL)
Symptom: Build fails with cinterop errors on modules like grpc, absl, leveldb, openssl_grpc, or other C++ transitive dependencies of Firebase.
Cause: discoverClangModulesImplicitly = true (the default) makes Kotlin attempt cinterop on every Clang module in the dependency graph, including C++ modules that are not compatible.
Solution: Set discoverClangModulesImplicitly = false and explicitly list only the Firebase Clang modules you need:
swiftPMDependencies {
discoverClangModulesImplicitly = false
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.6.0"),
products = listOf(product("FirebaseAnalytics"), /* ... */),
importedClangModules = listOf("FirebaseAnalytics", "FirebaseCore", /* ... */),
)
}See common-pods-mapping.md for the full importedClangModules reference.
---
Firebase Classes Not Found (Wrong Clang Module Name)
Symptom: Unresolved reference for Firebase classes like FIRDatabase, FIRRemoteConfig, FIRFirestore, FIRInAppMessaging even though the product is listed.
Cause: Several Firebase products expose ObjC headers through Clang modules whose names differ from the SPM product name. Using the product name in importedClangModules won't find the headers.
Solution: Use the correct internal Clang module names:
| SPM Product | Correct importedClangModules entry |
|---|---|
| FirebaseDatabase | FirebaseDatabaseInternal |
| FirebaseFirestore | FirebaseFirestoreInternal |
| FirebaseInAppMessaging-Beta | FirebaseInAppMessagingInternal |
| FirebaseRemoteConfig | FirebaseRemoteConfigInternal |
---
FirebaseFirestore Import Errors
Symptom: Can't import FIRFirestore classes
Cause: Firestore's Clang module name differs from product name. The internal Clang module exposed to Objective-C is FirebaseFirestoreInternal, not FirebaseFirestore.
Solution: Add explicit importedClangModules:
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.6.0"),
products = listOf(product("FirebaseFirestore")),
importedClangModules = listOf("FirebaseFirestoreInternal"), // Required
)---
Firebase Crashlytics: dSYM Upload Script Broken After Migration
Symptom: Crash reports don't appear in Firebase Console after migrating to SPM. Or the build phase fails with "No such file" errors referencing ${PODS_ROOT}/FirebaseCrashlytics/upload-symbols.
Cause: The CocoaPods-era dSYM upload script references ${PODS_ROOT} which no longer exists. The SPM equivalent is at a different path.
Solution: Update the existing "Run Script" build phase (or add one at the END if none exists):
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"With input files:
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)Also set Debug Information Format to DWARF with dSYM File for all build configurations in Build Settings.
---
Firebase Beta Products: SPM Name Differs
Symptom: FirebaseInAppMessaging or FirebaseAppDistribution not found as SPM product
Cause: Beta products have a -Beta suffix in SPM.
Solution: Use the correct SPM product name:
FirebaseInAppMessaging→FirebaseInAppMessaging-BetaFirebaseAppDistribution→FirebaseAppDistribution-Beta
---
dyld Crash When Mixing Firebase Across CocoaPods and SPM
Symptom: App crashes at launch with a dyld error like:
Symbol not found: _OBJC_CLASS_$_FIRFirestoreor similar _OBJC_CLASS_$_FIR* symbol-not-found errors. The Gradle build and Xcode compilation both succeed, but the app crashes at runtime.
Cause: Some Firebase pods were migrated to SPM while others remained in CocoaPods. All Firebase products share transitive dependencies (gRPC, abseil, leveldb, BoringSSL, nanopb). Having both package managers link these transitive dependencies causes duplicate/conflicting symbols that the dynamic linker cannot resolve.
Solution: Migrate all Firebase pods to SPM at once. This includes Swift-only pods (FirebaseAI, FirebaseFunctions, FirebaseMLModelDownloader) that Kotlin cannot use directly — add them as products entries without importedClangModules:
products = listOf(
// ObjC pods used by Kotlin:
product("FirebaseAnalytics"),
product("FirebaseAuth"),
// ...
// Swift-only pods (no importedClangModules needed):
product("FirebaseAI"),
product("FirebaseFunctions"),
),After adding new products, re-run integrateLinkagePackage to regenerate the linkage Swift package.
---
Firebase Initialization Fails at Runtime
Symptom: App crashes on Firebase initialization
Solution: 1. Ensure GoogleService-Info.plist is in iOS app target 2. Call FIRApp.configure() before using any Firebase service 3. Check Firebase console for configuration issues
---
Google Maps Issues
GoogleMaps Version Not Found
Symptom: SPM can't resolve GoogleMaps package
Solution: GoogleMaps requires exact version matching:
swiftPackage(
url = url("https://github.com/googlemaps/ios-maps-sdk.git"),
version = exact("10.6.0"), // Must use exact(), not from()
products = listOf(
product("GoogleMaps", platforms = setOf(iOS()))
),
)Check releases page for valid versions.
---
KSP (Kotlin Symbol Processing) Compatibility
KSP after updating Kotlin version
KSP should generally work with the target Kotlin version without any changes. Do NOT update KSP as part of the migration — it is out of scope.
If KSP does fail (unlikely), the issue is unrelated to the CocoaPods-to-SwiftPM migration itself. Present the error to the user and let them decide how to handle it separately.
---
Manual Integration Command Discovery
If the xcodebuild approach in Phase 5.1 fails, discover paths manually and run integration tasks directly:
# Find iOS project directory (contains Podfile)
IOS_DIR=$(dirname "$(find . -name "Podfile" -type f | head -1)")
# Find .xcodeproj (exclude Pods.xcodeproj) - use realpath for absolute path
XCODEPROJ=$(realpath "$(find "$IOS_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | grep -v Pods | head -1)")
# Find KMP module with swiftPMDependencies (module directory name)
KMP_MODULE=$(grep -rl "swiftPMDependencies" --include="build.gradle.kts" . | head -1 | xargs dirname | xargs basename)
XCODEPROJ_PATH="$XCODEPROJ" \
GRADLE_PROJECT_PATH=":$KMP_MODULE" \
./gradlew ":$KMP_MODULE:integrateEmbedAndSign" ":$KMP_MODULE:integrateLinkagePackage"---
Manual CocoaPods Deintegration from pbxproj
If pod deintegrate is not available, manually remove these CocoaPods references from project.pbxproj:
Pods_<target>.frameworkbuild file and file referencePods-<target>.debug.xcconfig/Pods-<target>.release.xcconfigfile referencesPodsgroup andFrameworksgroup (if it only contained the Pods framework)[CP] Check Pods Manifest.lockshell script build phase[CP] Embed Pods Frameworksshell script build phasebaseConfigurationReferencelines pointing to Pods xcconfig files
---
Manual Xcode Integration Steps
If the automatic integrateEmbedAndSign / integrateLinkagePackage tasks fail, set up the Xcode project manually:
1. Open .xcodeproj (or .xcworkspace if non-KMP CocoaPods remain) 2. Add "Compile Kotlin" run script phase BEFORE "Compile Sources":
cd "$SRCROOT/.."
./gradlew :moduleName:embedAndSignAppleFrameworkForXcode3. Set ENABLE_USER_SCRIPT_SANDBOXING = NO (Build Settings → Build Options → User Script Sandboxing) 4. Run ./gradlew --stop to restart the Gradle daemon after changing sandboxing 5. Add local package: ../moduleName/KotlinMultiplatformLinkedPackage
---
When Build Fails After Migration
Do NOT revert the migration as a first response. Instead:
1. Read the full error log — identify the actual failure type (Gradle resolution, import not found, linker error, Xcode build phase) 2. Re-check each migration phase — walk through Phases 2-6 and verify each step was applied. Common mistakes:
- Missing JetBrains Maven repo in
settings.gradle.kts - Wrong
groupor module name in import namespace (dashes not converted to dots) cocoapods {}block or plugin not fully removed (Phase 6)- Wrong Xcode project file opened (
.xcodeprojwhen non-KMP CocoaPods remain and.xcworkspaceis needed, or vice versa) isStatic = truemissing from framework config (required with dev.gitlive or similar CocoaPods-era wrapper klibs)integrateLinkagePackagenot run- EmbedAndSign disabler code not removed (prevents
integrateEmbedAndSign) embedAndSignAppleFrameworkForXcodecommented out in Xcode build phasecocoapods.*imports replaced that should have been preserved (bundled klib from third-party library)
3. Consult the sections above for specific error patterns 4. If unsure, present options to the user — describe what the logs show, list possible causes, and let the user decide
---
Rollback Instructions (Last Resort)
Only revert if analysis above does not resolve the issue:
Step 1: Restore Git Files
# Restore CocoaPods files (adjust path if iOS project is not in iosApp/)
git checkout -- "**/Podfile" "**/Podfile.lock"
git checkout -- *.podspec
git checkout -- **/build.gradle.kts
git checkout -- **/src/**/*.ktStep 2: Restore CocoaPods in build.gradle.kts
plugins {
kotlin("native.cocoapods") // Re-add
}
kotlin {
cocoapods {
// Restore original configuration
}
// Remove swiftPMDependencies block
}Step 3: Restore Kotlin Imports
Change all imports back:
// FROM:
import swiftPMImport.group.module.ClassName
// TO:
import cocoapods.PodName.ClassNameStep 4: Reinstall CocoaPods
# Navigate to directory containing Podfile (adjust path as needed)
cd <ios-project-directory> # e.g., iosApp/, ios/, or project root
pod installStep 5: Open Workspace
Open *.xcworkspace (not .xcodeproj) from the iOS project directory in Xcode.
---
Getting Help
If issues persist:
1. Check sample projects:
- kmp-with-cocoapods-compose-sample (spm_import branch)
- kmp-with-cocoapods-firebase-sample (spm_import branch)
2. Run verbose build:
./gradlew build --info3. Check generated files:
- Look in
moduleName/KotlinMultiplatformLinkedPackage/for Package.swift
4. Inspect klib contents using the klib tool (docs):
# Dump all API signatures from a klib
klib dump-metadata-signatures /path/to/library.klib
# Search for specific classes
klib dump-metadata-signatures /path/to/library.klib | grep "ClassName"
# Compare before/after — find klibs in build output
find . -name "*.klib" -path "*swiftPMImport*" # new swiftPMImport klibs
find ~/.gradle/caches -name "*.klib" -path "*libraryName*" # third-party klibsThis is particularly useful for verifying which classes are available in the swiftPMImport klib vs. bundled in third-party dependency klibs.
Related skills
How it compares
Use kotlin-tooling-cocoapods-spm-migration for guided KMP CocoaPods-to-SPM refactors; rely on generic mobile skills only when the stack is not Kotlin Multiplatform.
FAQ
What Kotlin version does kotlin-tooling-cocoapods-spm-migration require?
kotlin-tooling-cocoapods-spm-migration requires Kotlin 2.4.0-Beta2 or later, the first public release with swiftPMDependencies support on Maven Central. The skill also lists Xcode 16.4 or 26.0+ as a prerequisite for SPM integration.
What files change during a CocoaPods to SPM KMP migration?
kotlin-tooling-cocoapods-spm-migration updates Gradle kotlin("native.cocoapods") configuration to swiftPMDependencies {}, replaces pod() with swiftPackage(), maps cocoapods.* imports to swiftPMImport.*, and reconfigures the Xcode project for SPM.