Swift-to-Kotlin Interop

Patterns, tradeoffs, and concrete examples for calling Kotlin from Swift and using Swift libraries from Kotlin.

Overview

Kotlin and Swift do not interoperate directly at the language level; the practical integration surface is the platform ABI and generated Objective‑C headers or Swift modules produced by tooling.

Over the last few years the Kotlin toolchain has evolved: Kotlin/Native historically exported Objective‑C headers for Swift to import, and newer experimental features aim to export idiomatic Swift modules directly.


Common Approaches

Approach How it works When to use Key limitation
Objective‑C bridge Kotlin/Native generates Objective‑C headers; Swift imports them. Stable, widely supported for KMM iOS frameworks. Swift-only types and modern APIs can be awkward.
Swift export (experimental) Kotlin exports Swift modules directly for idiomatic Swift usage. When you want cleaner Swift APIs from Kotlin modules. Experimental; feature coverage still growing.
Swift wrapper + cinterop Write a Swift wrapper that exposes C/Objective‑C ABI; consume via Kotlin cinterop. Use Swift-only libraries (no Obj‑C API) from Kotlin. Extra wrapper layer and build complexity.
API generator (SKIE) Post-processes generated headers to produce Swift-friendly APIs and coroutine mappings. Improve ergonomics for large KMP frameworks used by Swift teams. Adds a build step and dependency on the generator tool.

Note: Kotlin Multiplatform Mobile (KMM) remains the primary path for sharing business logic; interoperability choices affect developer ergonomics on the iOS side.


Concrete Examples

1. Kotlin → Swift via Objective‑C headers

Build a Kotlin/Native framework; the compiler emits an Objective‑C header you import in Swift. Typical Kotlin declarations become Objective‑C types and functions with mangled names that Swift can call.

// Kotlin (shared/src/nativeMain/kotlin/com/example/Greeter.kt)
package com.example

class Greeter(val name: String) {
  fun greet(): String = "Hello, $name"
}
          

In Swift you import the generated umbrella header and call the class. The mapping is mechanical but sometimes loses Kotlin idioms.

2. Swift-only library used from Kotlin (wrapper + cinterop)

When a Swift library exposes no Objective‑C API, create a thin Swift wrapper that exposes C-compatible functions or Objective‑C classes, then use Kotlin/Native cinterop to bind them. This is the recommended workaround for Swift-only frameworks.

// Swift wrapper (MyCryptoBridge.swift)
@objc public class MyCryptoBridge: NSObject {
  @objc public static func sha256Hex(_ input: String) -> String {
    // call Swift CryptoKit and return hex
  }
}
          

3. Using Swift export (experimental)

Enable Swift export in Kotlin Gradle settings to generate a Swift module that preserves package names and produces cleaner imports. This reduces Objective‑C artifacts and improves naming. Use cautiously while the feature is experimental.

4. Improve ergonomics with SKIE

Tools like SKIE transform generated headers into idiomatic Swift APIs, map Kotlin coroutines to Swift async/AsyncSequence, and make Flow feel native in Swift. This is valuable when shipping large shared frameworks to Swift teams.


Tradeoffs and Practical Guidance

  • Stability vs ergonomics — Objective‑C bridge is stable; Swift export and API generators improve ergonomics but add risk or build complexity.
  • Build complexity — Wrappers and cinterop require extra build steps and CI changes; plan for versioning and binary compatibility.
  • Type fidelity — Some Kotlin types (sealed classes, inline classes, coroutines) need careful mapping to Swift equivalents or helper APIs.
  • Testing — Add integration tests that exercise the Swift-facing API surface to catch mapping regressions early.