skip to content

How is a Kotlin/Native module exposed to Swift, and what limitations of the Kotlin→Objective-C/Swift bridge should you design around?

level: seniorimportance: should knowfreq 35%

answer

  1. Kotlin/Native → Obj-C framework + umbrella header → Swift imports it
  2. Generics erased, sealed→no Swift enum, suspend→async/completion
  3. Default args dropped; value classes/unsigned/Result map poorly
  4. @ObjCName / @HiddenFromObjC / @ShouldRefineInSwift to shape API
  5. SKIE for sealed/enums/Flow/suspend ergonomics

basics

~20 s

Kotlin/Native compiles your code into an Objective-C-compatible framework that Swift can import. Swift sees Kotlin classes and functions, but some Kotlin features (like generics, sealed types, default arguments) don't survive the bridge cleanly, so you design the public API around that.

solid answer

~40 s

Kotlin/Native produces an **Objective-C framework** (a `.framework` with a generated umbrella header); Swift imports it like any Obj-C framework. Kotlin `public` declarations become Obj-C/Swift symbols: classes → classes, top-level functions → helper class methods, `object`/`companion` → `shared`/`companion` accessors. Limitations to design around: **generics** are largely erased to `Any`-like types on the Swift side; **sealed classes / when-exhaustiveness** don't map to Swift enums; **suspend functions** are exposed as completion-handler/async callbacks (Swift `async` via the bridge); **default arguments** disappear (overloads or explicit args needed); **inline/value classes, Kotlin Result, unsigned types** map poorly; and Swift name collisions force renaming. The remedy is a deliberate, flattened public API surface (`@ObjCName`, `HiddenFromObjC`, simple types) — often via SKIE or hand-written facades.

code

kotlin · 12 lines
kotlin
// Shape the bridged surface for Swift consumers
import kotlin.experimental.ExperimentalObjCName
import kotlin.native.HiddenFromObjC

@OptIn(ExperimentalObjCName::class)
class Greeter {
    @ObjCName("greetWithName")           // clean Swift selector
    fun greet(name: String): String = "Hi, $name"

    @HiddenFromObjC                       // keep internals out of the bridge
    internal fun debugDump(): Map<String, Any?> = emptyMap()
}

go deeper

for a junior

Knows Kotlin/Native builds a framework Swift can import.

for a middle

Can build and consume the framework and names a couple of mapping rules (top-level funcs → wrapper class, object → shared).

for a senior

Enumerates concrete bridge limitations (generics, sealed, suspend, defaults) and uses @ObjCName/@HiddenFromObjC/SKIE to shape the surface.

for a principal

Sets API-design policy for the shared module so the Swift SDK stays clean, versioned, and ergonomic despite the Obj-C bridge.

## How exposure works When you build a Kotlin/Native **framework** target (`binaries { framework { } }`), the compiler emits an **Objective-C framework**: a `.framework` directory containing a generated **umbrella header** declaring Obj-C interfaces for your `public` Kotlin API. Because Swift interops seamlessly with Obj-C, **Swift imports the framework** and calls Kotlin. ### Mapping basics - Kotlin **class** → Obj-C/Swift class. - Kotlin **top-level function/property** → methods on a generated wrapper class (named after the file, e.g. `UtilKt`). - **`object`** → exposed via a `shared` accessor; **`companion`** via `companion`. - **`interface`** → Obj-C protocol. - **enum** → Obj-C-style class with static members. ## Limitations to design around The bridge is **Objective-C-shaped**, so Kotlin features richer than Obj-C degrade: - **Generics**: Kotlin generic classes are exposed but type parameters are mostly **erased** on the Swift side; Swift sees them loosely typed. Variance and `where` bounds don't carry over well. - **Sealed classes / exhaustive `when`**: there's no Swift enum equivalent; you lose compile-time exhaustiveness. Swift sees a base class + subclasses. - **`suspend` functions**: exposed as **completion-handler** APIs; modern Kotlin/Native bridges them to Swift **`async`/await** with an error callback. Cancellation and structured concurrency don't fully translate. - **Default argument values**: **dropped** — Swift callers must pass every argument or you provide overloads. - **`inline value` classes, `UInt`/unsigned, `Result`, `Pair`/`Triple`**: map awkwardly or not at all. - **Flow/StateFlow**: not natively consumable; needs adapters (callbacks or libraries). - **Name collisions**: Obj-C has a flat selector namespace, so overloads/same-named members may collide and need renaming. ## Tools to shape the surface - **`@ObjCName("…")`** — control the exported Obj-C/Swift name. - **`@HiddenFromObjC`** — hide a declaration from the bridge. - **`@ShouldRefineInSwift`** + a Swift extension — present a cleaner Swift API over the raw bridged one. - **SKIE** (a popular Gradle plugin) — auto-generates Swift-friendly wrappers for sealed classes, enums, Flows, and suspend functions. ## Design guidance Treat the **public Kotlin API as an SDK surface for Obj-C**: keep it **flat and concrete** — plain classes, non-generic signatures, simple parameter types, no defaults, callbacks/`async` for async. Hide rich internals and expose a hand-curated facade. This keeps the Swift consumer experience clean despite the Obj-C-level bridge. ```kotlin binaries { framework { baseName = "Shared" // export(project(":domain")) to re-export deps } } ```

  • How are suspend functions consumed from Swift?
    They're exposed as completion-handler methods that the modern bridge surfaces as Swift async/await, with errors delivered via the error parameter; full structured-concurrency cancellation doesn't carry over.
  • Why don't Kotlin default arguments work from Swift?
    The Obj-C bridge has no notion of default parameter values, so each must be supplied; you add overloads or factory functions to compensate.
  • What does SKIE add over the raw Kotlin/Native bridge?
    Swift-friendly wrappers for sealed classes (as Swift enums), enums, suspend functions, and Flows, generated at build time.

The Kotlin→Swift bridge speaks through an Objective-C interpreter who only knows older vocabulary: rich Kotlin idioms get paraphrased or lost, so you write your public sentences in words the interpreter knows.

saying these in an interview costs you the question

  • Assuming Kotlin generics retain their type parameters in Swift
  • Exposing sealed classes and expecting Swift exhaustiveness
  • Relying on default arguments being visible to Swift callers
  • Exporting a huge rich API instead of a curated facade
  • Not knowing the bridge goes through Objective-C

context