Swift packages in Kotlin Multiplatform – built-in SwiftPM import with Kotlin 2.4.x

Swift packages in Kotlin Multiplatform — built-in SwiftPM import with Kotlin 2.4.x

In 2026, creating a cross-platform mobile app with shared business logic and UI between Android and iOS is fairly straightforward thanks to Kotlin Multiplatform and Compose Multiplatform. However, sooner or later we might need a platform-specific library that cannot be added to commonMain, and suddenly things aren’t as straightforward as we would expect.

Google Nearby Connections is a good example. Its iOS SDK is distributed as a Swift package, but downloading and compiling it doesn’t necessarily mean that its APIs will be accessible from Kotlin. There are a few additional pieces involved, and Kotlin 2.4.0 made the configuration easier thanks to built-in SwiftPM import.

In this article, I’ll describe how I migrated an existing spm4Kmp integration to this approach, what could be removed and why some Swift code still had to stay.

Offline messaging and audio calls with Flight Chat

Flight Chat is a peer-to-peer offline messenger built with KMP and CMP. It allows iOS and Android devices to exchange text messages and make audio calls without an Internet connection or a server in between. Google’s Nearby Connections SDK handles communication, while the application shares its business logic and UI.

You can try Flight Chat on Google Play or the App Store.

The native SDK integration sits behind platform-specific implementations, which is quite convenient for this migration — changing how the iOS dependency is built doesn’t require changes in ViewModels or Composables.

Flight Chat — offline text messages and audio calls between iOS and Android

Swift interop — there is still Objective-C underneath

The original integration used the spm4Kmp Gradle plugin to handle Swift package dependencies and the build configuration needed to make compatible APIs available to Kotlin. Moreover, it supported local Swift bridge code, which was crucial for integrating the Nearby SDK.

The important word here is “compatible”. Kotlin/Native’s Objective-C interoperability doesn’t expose arbitrary Swift APIs, so a Swift package can compile successfully while the generated Kotlin bindings contain almost nothing useful. For someone used to simply adding a third-party dependency and using its classes, this was a somewhat unpleasant surprise. I encountered exactly this situation and opened an issue, where François (the plugin’s author) helped me understand the interoperability limitations. The discussion also became the plugin’s first FAQ entry, so hopefully it saved someone else a similar surprise.

The Nearby SDK includes a Swift-facing NearbyConnections product and a lower-level NearbyCoreAdapter module. You can see how Google’s Swift API uses that module in its Advertiser implementation. My local Swift wrappers also call NearbyCoreAdapter directly, while exposing Objective-C-compatible APIs to Kotlin. For example, my advertiser wrapper inherits from NSObject and uses @objc and @objcMembers to expose its compatible declarations. Its advertising methods then call GNCCoreAdapter.

Package dependencies: nearbyIos depends on NearbyConnections, which then brings in NearbyCoreAdapter. API calls: Kotlin calls the Objective-C-visible nearbyIos Swift wrappers, which call NearbyCoreAdapter.

The package we depend on and the APIs we call are not quite the same thing. This distinction matters because replacing the package integration doesn’t remove the need for Objective-C-compatible APIs. The wrappers remain necessary with either build setup.

Kotlin 2.4.x — missing module & SwiftPM import

Unfortunately, after upgrading from Kotlin 2.3.20 to 2.4.0, the Swift package still compiled, but the subsequent cinterop task failed with:

fatal error: module 'NearbyCoreAdapter' not found

The generated Swift header referenced NearbyCoreAdapter, yet the cinterop configuration didn’t include the path needed to resolve that transitive module. Adding its build directory to compilerOpts worked around the problem, although maintaining a path into another tool’s build output wasn’t particularly appealing.

I reported the issue, but we couldn’t reproduce it in the plugin’s example project. Thankfully, the same Kotlin release introduced an alternative integration, so I decided to try it.

And just a side-note — SwiftPM import and Swift export are separate features:

  • The former imports compatible APIs from Swift packages into Kotlin.
  • The latter exposes Kotlin APIs to Swift.

The Swift export alpha announcement doesn’t mean that Kotlin can suddenly call every Swift API. Objective-C is still alive.

Build configuration — a local Swift package

With spm4kmp, the swiftPackageConfig block declared the Nearby dependency, configured the Swift bridge and prepared the package consumed by Xcode. With the new integration, the wrappers became a regular local Swift package with their own Package.swift.

The manifest declares a library product called nearbyIos, a target containing the existing Swift files and a dependency on Google’s repository. The target then depends on the NearbyConnections product from that package.

There are two concepts worth distinguishing here: a SwiftPM product is what another package consumes, while a target describes the source module and its dependencies. In this local package, both are named nearbyIos, which makes the configuration fairly straightforward.

On the Gradle side, we point to that package and select the module to import:

You may notice that products and importedClangModules contain the same name. They serve different purposes: the former selects the SwiftPM product, while the latter selects the Clang module whose APIs should be imported. Setting discoverClangModulesImplicitly to false makes that selection explicit instead of automatically discovering available modules.

Xcode integration — Gradle is only part of the setup

Having Kotlin bindings available doesn’t finish the integration. The iOS application also needs the corresponding native code linked into it.

Kotlin’s tooling generates an intermediary package called KotlinMultiplatformLinkedPackage. In this project, it depends on the local nearbyIos package and replaces the previous exportedNearbyIos reference in Xcode. The official integration guide describes the :integrateLinkagePackage Gradle task, which generates the package and updates the Xcode project.

The generated linkage package connects the Swift dependencies to the iOS app

The generated package, updated Xcode project and the SwiftPM resolution lock can be committed to version control. The lock records the resolved dependency revisions, including transitive ones.

On the Kotlin side, the generated package name changed as well:

-import nearbyIos.Advertiser
+import swiftPMImport.projectName.moduleName.Advertiser

One additional CI adjustment was to set rootProject.name explicitly in settings.gradle.kts. It’s worth checking this setting when applying the migration to your own project.

Was the migration worth it?

For this particular integration, definitely. The initial migration worked on the first try, and I could remove both the plugin and the temporary cinterop workaround while retaining the existing Swift bridge.

However, keep in mind that built-in support doesn’t mean every Swift package will work without additional effort — SwiftPM import is still documented as alpha. The migration used Kotlin 2.4.0; Kotlin 2.4.21 is available at the time of writing. Check the current integration docs for the Kotlin version you use.

If your existing integration works well, I wouldn’t migrate just for the sake of it. In my case, an unresolved integration issue and the opportunity to simplify the setup made it a worthwhile change. After all, spm4Kmp had already done the heavy lifting of making the original integration possible.

Hopefully these configuration examples save you some time when the next Swift dependency compiles successfully, but Kotlin still claims it has never heard of it!


Swift packages in Kotlin Multiplatform – built-in SwiftPM import with Kotlin 2.4.x was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.