This project demonstrates how to integrate a Swift Package Manager (SwiftPM) dependency—exposing an Objective-C compatible API—into the iosMain source set of a Kotlin Multiplatform (KMP) project. The SwiftPM logic is then consumed by Kotlin/Native code and ultimately used within a native iOS application.
The primary goal is to establish seamless interoperation across three layers:
- SwiftPM — Swift logic packaged and distributed using Swift Package Manager
- Kotlin/Native (KMP) — Kotlin code accessing Swift functionality via Objective-C wrappers using CInterop
- iOS App — A native iOS application consuming Kotlin logic that internally calls SwiftPM logic
This prototype provides a complete, working example of:
- Integrating a SwiftPM dependency in a Kotlin/Native iOS target
- Exposing SwiftPM logic to Kotlin via Objective-C and CInterop
- Packaging the Kotlin code as an
XCFramework - Using the
XCFrameworkin an iOS Swift app to access the full Kotlin → Objective-C → Swift call chain
The following diagram outlines the call chain and structural dependencies across the iOS application, Kotlin Multiplatform (KMP) module, and SwiftPM package:
iOS App
└── DummyPackageIOS.swift (Swift wrapper)
└── Kotlin XCFramework (ComposeApp)
└── DummyIosPackage.kt (iosMain implementation)
└── SwiftPackageObjC (Objective-C wrapper)
└── SwiftPackage.swift (Core Swift logic)
The Swift Package Manager (SwiftPM) dependency used in this prototype is located at:
/SwiftPMPackages/SwiftPackage/
It consists of two modules:
SwiftPackageSwift: Contains the core logic written in pure Swift.SwiftPackageObjC: Provides an Objective-C wrapper that exposes Swift APIs in a way that Kotlin/Native can access via CInterop.
SwiftPackage/
├── Package.swift
└── Sources/
├── SwiftPackageSwift/
│ └── SwiftPackage.swift
└── SwiftPackageObjC/
├── SwiftPackageObjC.m
├── SwiftPackageObjC.h
└── include/module.modulemap
- The
SwiftPackageSwift class defines a basic function returning a static string or message. - The Objective-C wrapper (
SwiftPackageObjC) exposes that logic through an@interface, making it accessible to Kotlin/Native through Objective-C headers. - A
module.modulemapis provided in theinclude/directory to describe the Objective-C interface for CInterop. - The SwiftPM package is built as a dynamic library to ensure runtime symbol visibility when consumed by Kotlin/Native.
The Kotlin Multiplatform (KMP) module forms the core layer that bridges the SwiftPM dependency with the iOS app.
commonMaindefines a platform-agnostic interface,DummyPackage.iosMainprovides the actual implementation,DummyIosPackage, which internally calls the Objective-C wrapper (SwiftPackageObjC) exposed by the SwiftPM module.
//commonMain
interface DummyPackage {
fun describe(): String
}
expect fun getDummyPackage(): DummyPackage
// iosMain
import SwiftPackage.SwiftPackageObjC
class DummyIosPackage : DummyPackage {
private val spm = SwiftPackageObjC()
override fun describe(): String {
return spm.describe()
}
}
actual fun getDummyPackage(): DummyPackage = DummyIosPackage()
The SwiftPM Objective-C wrapper is integrated into the Kotlin/Native build using CInterop. This is defined in composeApp/build.gradle.kts:
// composeApp/build.gradle.kts
val SwiftPackage by cinterops.creating {
definitionFile.set(project.file("src/nativeInterop/cinterop/SwiftPackage.def"))
includeDirs(project.file("path/to/SwiftPackageObjC/include"))
}This configuration tells Kotlin/Native to generate interop bindings for the Objective-C headers defined in the .def file and located in the specified includeDirs.
The Kotlin codebase is compiled into a multiplatform XCFramework, which is then consumed by the iOS application.
The native iOS application integrates the Kotlin XCFramework (ComposeApp) and serves as the top layer of the interop stack.
To simplify usage of the Kotlin logic inside Swift, a lightweight Swift wrapper class is defined. This wrapper calls the Kotlin getDummyPackage() function, which returns an instance of DummyIosPackage—the platform-specific implementation from the iosMain source set.
While wrapping Kotlin logic in Swift is optional, doing so helps encapsulate interop boundaries and provides a cleaner API for Swift-side consumption.
@objc public class DummyPackageIOS: NSObject {
private let dummyKmpPackage = DummyPackage_iosKt.getDummyPackage()
@objc public func describe() -> String {
return dummyKmpPackage.describe()
}
}This class acts as a bridge between the Swift-based UI and the Kotlin business logic, which itself indirectly invokes SwiftPM logic via the Objective-C wrapper.
This section summarizes the core technologies involved in the prototype and how they interoperate to enable a seamless Kotlin-to-Swift integration via Objective-C.
Kotlin Multiplatform (KMP) is a technology that enables code sharing across multiple platforms—such as iOS, Android, desktop, and web—while allowing you to retain native code for platform-specific functionality.
KMP organizes code into:
commonMain: Shared business logic compiled for all declared targets- Platform-specific source sets like
iosMainorandroidMain, where native code or interop bindings reside
Learn more in JetBrains's official documentation.
The Swift Package Manager (SwiftPM) is Apple’s official tool for distributing, compiling, and managing Swift libraries. It is tightly integrated with Xcode and supports modular, dependency-managed Swift projects.
It has been the default packaging tool for Swift since version 3.0.
Learn more in SwiftPM's official documentation.
Kotlin/Native does not directly interoperate with Swift. However, it supports interop with Objective-C. Since Swift and Objective-C are also interoperable, this allows Kotlin to access Swift code indirectly through Objective-C.
Key enabling mechanism:
-
Swift APIs must be annotated with
@objcto be visible to Objective-C. -
Kotlin/Native can use these Objective-C APIs via CInterop.
This creates the following valid transitive interop chain:
(Kotlin/Native ↔ ObjC) ∧ (ObjC ↔ Swift) ⇒ (Kotlin/Native ↔ Swift)
See following documentations/resources to learn/understand more about the interop:
- What is Kotlin/Native?
- What is Objective-C (ObjC)?
- What is Swift?
- Objective-C ↔ Swift Interoperability
- Objective-C/Swift ↔ Kotlin/Native Interoperability
- Apple system libraries like
FoundationorCoreBluetoothare natively supported in Kotlin/Native via prebuilt bindings—no extra setup required. - Third-party iOS frameworks can also be used if they:
- Are written in Objective-C, or
- Are written in Swift and expose an Objective-C-compatible interface (via
@objc)
- Pure Swift APIs (not annotated with
@objc) are not usable from Kotlin/Native due to Swift’s lack of stable ABI and header exposure. - Integration must be configured manually using
.deffiles and properincludeDirs.
Note: To use a SwiftPM dependency in Kotlin/Native, it must:
- Provide Objective-C headers, and
- Expose Swift APIs using
@objcannotations or an Objective-C wrapper.
This section outlines how to:
- Integrate a local SwiftPM package with Objective-C headers into a Kotlin Multiplatform (KMP) project using CInterop.
- Build and consume a Kotlin XCFramework from a native iOS application.
Follow these steps to expose a SwiftPM dependency (with Objective-C API) to Kotlin/Native:
-
Create a SwiftPM package in Xcode
- Set up your Swift package project using
Package.swift.
- Set up your Swift package project using
-
Configure SwiftPM for Objective-C interop
In your SwiftPM package:
- Create an
include/directory inside your Objective-C target (SwiftPackageObjC) under Sources/. - Place or symlink the Objective-C header files you want to expose in this directory.
- Add a
module.modulemapto theinclude/folder:module SwiftPackageObjC { header "SwiftPackageObjC.h" export * }
- Create an
-
Create a Kotlin CInterop definition file
Inside your KMP module (e.g.,
composeApp), create:src/nativeInterop/cinterop/SwiftPackage.defExample content:
headers = SwiftPackageObjC.h package = SwiftPackage language = Objective-CNOTE : Role of
module.modulemapand.defFilesThese files are essential for making native libraries available to Kotlin/Native. The
module.modulemapinforms the compiler how to treat headers as a module, while.definstructs Kotlin how to link and expose them. -
Configure
build.gradle.ktsfor CInterop IncomposeApp/build.gradle.kts, register the interop binding:iosTarget.compilations.getByName("main") { val SwiftPackage by cinterops.creating { definitionFile.set(project.file("src/nativeInterop/cinterop/SwiftPackage.def")) includeDirs(project.file("${rootDir}/path/to/SwiftPackageObjC/include")) } }
-
Enable CInterop commonization in
gradle.propertiesto share interop configuration across iOS architectures:# Enables shared CInterop configuration across iOS architectures kotlin.mpp.enableCInteropCommonization=true
-
Sync Gradle and rebuild
After configuration, sync the Gradle project to generate the interop bindings.
Follow these steps to package your KMP module as a Kotlin XCFramework and use it in an iOS app:
-
Define the XCFramework in
composeApp/build.gradle.kts:val xcFramework = XCFramework("ComposeApp")
-
Declare iOS targets in your Kotlin setup:
iosX64() iosArm64() iosSimulatorArm64()
-
Configure framework binaries for each target:
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target -> target.binaries.framework { baseName = "ComposeApp" isStatic = true xcFramework.add(this) } }
-
Build the XCFramework using Gradle:
./gradlew :composeApp:assembleXCFramework
-
Import the XCFramework into your iOS project
- Import
ComposeApp.xcframeworkinto your Xcode project. - Make sure the framework is included in the appropriate build phase and linked correctly.
- Import
-
Call Kotlin code from Swift Example usage from Swift code:
import ComposeApp // Kotlin XCFramework let dummy = DummyPackageIOS() print(dummy.describe())
- JetBrains's official documentation
- SwiftPM's official documentation
- What is Kotlin/Native?
- What is Objective-C (ObjC)?
- What is Swift?
- Objective-C ↔ Swift Interoperability
- Objective-C/Swift ↔ Kotlin/Native Interoperability
- Migrating Obj-C/C++ module to SPM
- More about SwiftPM '/include' folder
- KMP iOS Integration Methods
- Exporting Modules as an XCFramework
- Creating a Standalone Swift Package with Xcode
If I had more time to allocate to this prototype, I would focus on the following enhancements and extensions:
-
Remote SwiftPM Dependency Integration (Objective-C APIs)
Explore using a remote SwiftPM package hosted on GitHub that exposes
@objc-compatible APIs. Specifically, I would investigate whether it’s possible to integrate such a dependency intoiosMainwithout relying on manually extracted headers or a local wrapper layer. -
Reduce Export Scope of Kotlin XCFramework
Modularize the Kotlin Multiplatform library to expose only the required class (
DummyIosPackage) in the XCFramework, instead of exporting the entireComposeAppmodule. This would result in a cleaner public API surface and reduced binary size. -
Improved Developer Tooling
Add scripts to lint Objective-C headers, detect breaking changes, and verify that generated binaries expose stable, ABI-compatible symbols for Kotlin/Native interop.
Throughout this prototype, I gained the following technical insights:
-
Kotlin/Native Interop Depends on Objective-C Symbols
Kotlin/Native cannot directly consume Swift code; interop requires Swift APIs to be exposed via Objective-C, typically through
@objcannotations or wrapper layers. -
Role of
module.modulemapand.defFilesThese files are essential for making native libraries available to Kotlin/Native. The
module.modulemapinforms the compiler how to treat headers as a module, while.definstructs Kotlin how to link and expose them. -
XCFramework Export Workflow
I learned how to configure and build a Kotlin Multiplatform XCFramework, and how to import it into a native iOS app. This included coordinating Gradle multiplatform configuration, linking native targets, and wrapping Kotlin logic for Swift-side access.
-
Gained practical experience with Kotlin Multiplatform Gradle setup, iOS targets, and XCFramework creation.
Based on the technical insights gained during this prototype, the following research directions are strong candidates for future work.
- Goal: Make Kotlin XCFrameworks (produced from Kotlin/Native) consumable directly from SwiftPM packages.
- Tasks may include:
- Creating a sample Package.swift that includes a Kotlin-generated
.xcframeworkas a binary target. - Ensuring proper module mapping and visibility (via .modulemap).
- Investigating limitations for distributing Kotlin code via SwiftPM (e.g., target platform restrictions).
- Creating a sample Package.swift that includes a Kotlin-generated
Example:
.binaryTarget(
name: "ComposeApp",
path: "./binaries/ComposeApp.xcframework"
)-
Goal: Prototype a SwiftPM build tool plugin that can compile Kotlin/Native
.ktfiles directly during a SwiftPM build. -
Rationale: This would allow Swift developers to treat Kotlin codebases as source dependencies within SwiftPM.
Example Use Case:
A Swift package includes a Sources/Kotlin/ directory and a plugin builds it into a .framework or .xcframework automatically during SwiftPM resolution.
-
Goal: Create a Kotlin Gradle plugin or extension DSL that wraps common CInterop setup for SwiftPM use cases.
-
Motivation: The current
.def+ manual include dir setup is error-prone and verbose.
Example:
kotlinInterop {
register("SwiftPackage") {
headers = "SwiftPackageObjC.h"
includeDirs = "src/nativeInterop/include"
moduleMap = true
}
}
- See similar implementation here : GitHub Link.
This prototype demonstrates how to bridge Swift code into Kotlin Multiplatform projects by leveraging Objective-C compatibility and Kotlin/Native's CInterop tooling. It showcases a complete integration pipeline—from SwiftPM setup and Objective-C wrapping, to Kotlin interop and iOS app consumption.
With additional time, future efforts could focus on streamlining remote SwiftPM integration, modularizing exports, and automating cross-language packaging workflows via plugins and declarative metadata.

