Your Kotlin Multiplatform Module Can Finally Depend on Swift Packages
Every Kotlin Multiplatform team hits the same wall, usually in week two. The shared module is humming along — networking, persistence, business logic, all in commonMain — and then someone says the fatal words: “okay, now wire up Firebase Analytics.”
On Android, that’s a one-line Gradle dependency. On iOS, Firebase ships as a Swift package. And your Kotlin/Native code, the whole point of which is to run on iOS, has historically had no first-party way to depend on one. The ecosystem’s most important iOS SDKs — Firebase, Google Maps, half the payment and auth providers — live on Swift Package Manager, and KMP couldn’t reach them without ceremony.
With Kotlin 2.4.0, that changed. There’s now a first-party swiftPMDependencies block in the Kotlin Gradle plugin — SwiftPM import, currently in Alpha — that lets your KMP module declare Swift packages the same way it declares Maven dependencies. Let’s look at where we’ve been, how the new mechanism works, and where its edges are.
How we’ve coped until now
If you’ve shipped KMP to production, you’ve used at least one of these three workarounds. It’s worth naming them, because the new feature doesn’t kill all of them — and knowing the trade-offs tells you when to migrate.
1. Dependency inversion (the “clean” way)
Define an interface in commonMain, implement it in Swift, inject it at startup:
// commonMain
interface AnalyticsTracker {
fun track(event: String, params: Map<String, Any?>)
}
// iosApp — Swift side
class FirebaseTracker: AnalyticsTracker {
func track(event: String, params: [String: Any?]) {
Analytics.logEvent(event, parameters: params as [String: Any])
}
}
This works, and architecturally it’s fine — arguably good. But it has a hidden tax: every iOS SDK touchpoint needs an interface, a Swift implementation, and injection plumbing. Your “shared” module quietly stops being shared; the interesting integration logic lives in the iOS app where your Android teammates can’t see it, test it, or reuse it.
2. The CocoaPods plugin (the deprecated way)
The Kotlin CocoaPods Gradle plugin was the official answer for years — pod("FirebaseAnalytics") and go. But CocoaPods itself is in maintenance mode, the Apple ecosystem has decisively moved to SwiftPM, and an increasing number of SDKs don’t publish pods for new versions at all. Building your 2026 integration strategy on CocoaPods is building on a pier that the tide is visibly leaving.
3. Manual cinterop (the pain way)
Hand-write a .def file, point cinterop at an XCFramework you downloaded and version-control yourself, and maintain linker flags by hand. It works, it’s how the hardcore teams did Firebase-in-commonCode for years, and nobody who has done it wants to do it again.
The new way: swiftPMDependencies
Here’s the whole thing. In the build.gradle.kts of the module that declares your Apple targets:
kotlin {
iosArm64()
iosSimulatorArm64()
swiftPMDependencies {
swiftPackage(
url = url("https://github.com/firebase/firebase-ios-sdk.git"),
version = from("12.5.0"),
products = listOf(product("FirebaseAnalytics")),
)
}
}
That’s a Swift package, declared in Gradle, resolved by SwiftPM, with its headers imported into your Kotlin/Native compilation — transitive dependencies included, automatically.
Version constraints will feel familiar from both worlds:
version = from("12.5.0") // like Gradle's `require` — this version or newer
version = exact("10.3.0") // pin it exactly
version = branch("main") // track a git branch
version = revision("e74b0…") // pin a commit
And if a product only makes sense on some Apple platforms, you can constrain it:
swiftPackage(
url = url("https://github.com/googlemaps/ios-maps-sdk.git"),
version = exact("10.3.0"),
products = listOf(
product("GoogleMaps", platforms = setOf(iOS()))
)
)
Calling it from Kotlin
Imported APIs land in a namespace of the form swiftPMImport.<group>.<module>, and you use them from iosMain like any other Kotlin code:
// shared/src/iosMain/kotlin/Analytics.kt
import swiftPMImport.com.example.shared.FIRAnalytics
fun trackCheckout(total: Double) {
FIRAnalytics.logEventWithName(
"checkout_completed",
parameters = mapOf("total" to total)
)
}
Notice the class name: FIRAnalytics, not Analytics. That’s not a typo, and it’s the single most important thing to understand about this feature — more on it in the limitations section.
One-time setup
The first time you add SwiftPM dependencies, you run an integration task that generates a small synthetic Swift package and wires your Xcode project to link everything correctly:
XCODEPROJ_PATH='iosApp/iosApp.xcodeproj' \
./gradlew :shared:integrateLinkagePackage
Commit what it generates — including the merged lock file under .swiftpm-locks/ (the KMP equivalent of committing Package.resolved, so CI and teammates resolve identical versions). After that, adding or bumping dependencies is just editing Gradle and syncing.
Two practical notes from the docs worth internalizing before your first build fails mysteriously:
- Use a static framework. SwiftPM import has limited support for dynamic Kotlin/Native frameworks — if you see
Undefined symbolsor duplicate-class warnings at link time, setisStatic = trueon your framework binary. - Local packages work too. If your iOS team has in-house Swift packages,
localSwiftPackage(directory = …, products = …)pulls them in the same way. This is quietly huge for teams with internal design systems or networking layers on the Swift side.
The honest limitations
This feature is Alpha, and the docs are refreshingly upfront about the edges. Three matter:
1. You’re importing Objective-C APIs, not Swift APIs. This is the big one. SwiftPM import gives Kotlin visibility into the Objective-C surface of a package — the Clang modules. Firebase works beautifully because it’s largely Objective-C under the hood (hence FIRAnalytics). A pure-Swift package with no @objc surface will resolve, download, and link… and expose almost nothing you can call. Before migrating an integration, check what the SDK actually exposes to Objective-C. (Full Swift-to-Kotlin interop is the long game; Kotlin 2.4 also advanced the separate Swift export feature, which handles the opposite direction — exposing your Kotlin API to Swift without the Objective-C squint.)
2. You can’t both import and export. A KMP module that uses SwiftPM import can’t itself be exported as a Swift package yet — tracked as KT-84420. If your distribution story is “our shared module is a Swift package,” hold off.
3. Alpha means Alpha. The DSL may change, and you should expect rough edges around lock-file management in bigger multi-module setups. It’s absolutely ready to evaluate on a branch; whether it’s ready for your production build is a judgment call about your team’s tolerance.
So which approach do you use now?
My take, having lived all three workarounds:
- New SDK integration where the SDK has an Objective-C surface (Firebase, Google Maps, most analytics/ads SDKs): use SwiftPM import. The days of choosing between CocoaPods life support and interface plumbing are over.
- Pure-Swift SDKs: keep the dependency-inversion pattern. It was never wrong — it was just mandatory. Now it’s a choice you make for Swift-only surfaces and for things you genuinely want abstracted (payments providers you might swap, say).
- Existing CocoaPods-plugin setups: start planning the migration. Nothing is on fire today, but every quarter the pod ecosystem gets a little staler, and the replacement now officially exists.
The bigger story here is directional. For years, KMP’s iOS interop was the asterisk in every adoption pitch — “shared logic, yes, but talking to the iOS ecosystem gets weird.” Between SwiftPM import, Swift export, and stable Compose on iOS, JetBrains is systematically deleting the asterisks. This one was the oldest, and it’s the one I’d start experimenting with this week: pick your Firebase integration, move it behind swiftPMDependencies on a branch, and see how much Swift glue code you get to delete.
Sources: Adding Swift packages as dependencies to KMP modules (kotlinlang.org) · Kotlin 2.4.0 Released (JetBrains Blog) · Swift package export setup (kotlinlang.org)