Skip to content

필사 모드: Kotlin Swift Export Reaches Alpha — It Is Tearing Down the Objective-C Bridge, but Do Not Cross Yet

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction — The Tax KMP Pays on iOS

Kotlin Multiplatform's sales pitch has always been clean: write your business logic once, and Android and iOS share it. On the Android side that actually holds — same language, same toolchain.

The problem has always been the iOS boundary. What Kotlin/Native produces is not something Swift can read directly. Kotlin emits an Objective-C framework, and Swift retranslates that Objective-C header into an API it can see. In other words, wedged between Kotlin and Swift is a third language nobody asked for. Both Kotlin and Swift are modern languages that encode null-safety in their type systems, and the interpreter sitting between them is a language from 1984.

This translation cost is not abstract. As you'll see below, types get flattened, names get mangled, and coroutines regress into callbacks. Swift export is an attempt to remove this bridge outright, and it reached Alpha in Kotlin 2.4.0 (released June 3, 2026).

Reaching Alpha is good news. But once you check what "Alpha" precisely means in JetBrains's dictionary, the urge to file a migration ticket right now will fade. This post lays out both sides.

What the Objective-C Bridge Actually Breaks

First, we need to see why this feature exists at all — these are losses that Kotlin's own Objective-C interop documentation admits to.

Primitive boxing. In the documentation's own words, a box of kotlin.Int is represented in Swift as a KotlinInt class instance, and these classes derive from NSNumber. The reverse does not happen automatically — the docs state explicitly that NSNumber is not automatically converted to a Kotlin primitive when used as a Swift/Objective-C parameter or return type. The reason is that NSNumber does not statically carry information about whether the wrapped value is a Byte, a Boolean, or a Double, so it must be cast manually. Passing a single nullable integer produces a heap-allocated wrapper, and manual casting code on the Swift side.

Generic loss. Objective-C's "lightweight generics" are too narrow to hold Kotlin's generics. The docs pin down two constraints. First, generics can only be defined on classes, not on interfaces (Objective-C/Swift protocols) or functions. Second, nullability gets flattened. Taking the docs' own example, this Kotlin code

class Sample<T>() {
    fun myVal(): T
}

looks like this in Swift:

class Sample<T>() {
    fun myVal(): T?
}

T became T?, because the generated Objective-C header has no choice but to declare myVal as nullable. The workaround the docs recommend is to put an explicit T : Any upper bound so the header can mark it non-null — which is to say, twist your Kotlin API design to accommodate the Objective-C header's limitations.

Coroutines become callbacks. Kotlin's suspend functions appear in the generated Objective-C header as functions that take a callback — in Swift terms, a completion handler. Since Swift 5.5 there's been a path to call them as async functions, but the docs describe that path as "highly experimental" with specific constraints, and point to KT-47610 for details. As of the writing of this post, July 16, 2026, that issue is still Open. It's filed under the Language Design and Native subsystems, tagged ObjC Export. This issue, open for years now, condenses exactly why a separate path called Swift export was needed.

Name mangling. This is the motivation the Swift export docs state for themselves — getting rid of Objective-C's confusing underscores and mangled names. If you've ever seen top-level functions show up attached to some class like SharedKt, you know exactly what this means.

To sum up, the Objective-C bridge cannot carry a Kotlin API across as-is — it projects onto the subset that Objective-C can express. The loss was unavoidable by design.

What Swift Export Does

Swift export's idea is simple: remove the intermediate language.

The official documentation's opening line says it precisely — Swift export exports Kotlin source directly so it can be called idiomatically from Swift, and gets rid of the Objective-C header. The compiler generates the swiftmodule file, the static .a library, the header file, and the modulemap file directly, and copies them into the app's build directory.

Configuration is a single Gradle block.

// build.gradle.kts
kotlin {
    iosArm64()
    iosSimulatorArm64()

    swiftExport {
        // root module name
        moduleName = "Shared"

        // strip the package prefix from the generated Swift code
        flattenPackage = "com.example.sandbox"

        // export configuration for an external module
        export(project(":subproject")) {
            moduleName = "Subproject"
            flattenPackage = "com.subproject.library"
        }
    }
}

And in Xcode's Build Phases, you swap the Run Script's task from embedAndSignAppleFrameworkForXcode to embedSwiftExportForXcode. The script the docs give is ./gradlew :<module-name>:embedSwiftExportForXcode.

Some of the losses from the previous section genuinely go away. Here's what the docs list.

  • Better primitive nullability. Unlike Objective-C interop, which had to box types like Int? into a KotlinInt wrapper, Swift export converts nullability information directly.
  • Multi-module support. One Kotlin module maps to one Swift module.
  • Package preservation. Kotlin packages are explicitly preserved to avoid name collisions (translated into nested Swift enums).
  • Overloads. Kotlin's overloaded functions can be called from Swift without ambiguity.
  • Type aliases. typealias carries over into Swift as-is.

And two more things were added in Kotlin 2.3.0 (December 16, 2025). Kotlin enum class stopped going out as a plain Swift class the way it used to, and started mapping to a genuine native Swift enum; and vararg functions started mapping directly to Swift's variadic parameters.

What 2.4.0 Changed — the Promotion, and Coroutines

The Kotlin 2.4.0 release notes call out two things as the core of this change.

First, structured concurrency. Kotlin's suspend functions and suspend function types now go out as Swift's idiomatic async — not as completion handlers.

// Kotlin
suspend fun hello(): String {
    delay(1000)
    return "Hello Swift! This is Kotlin."
}
// Swift
let msg = try await hello()

Second, Flow export. This one matters more in practice. As the docs point out precisely, up until now the only way to expose kotlinx.coroutines.flow's Flow interface to Swift was a third-party solution. Now Flow goes out mapped to Swift's AsyncSequence by default, with type information preserved.

// Kotlin
fun flowOfStrings(): Flow<String> = flowOf("hello", "any", "world")
// Swift
var actual: [String] = []

// String type is correctly inferred from Kotlin
for try await element in flowOfStrings().asAsyncSequence() {
    actual.append(element)
}

Anyone who has actually run KMP in production knows the weight of that one line. "How do I plug a Kotlin-side state stream into SwiftUI" has always been a question every team adopting KMP had to face, and the answer up to now was always a third-party wrapper.

It's also worth noting this is on by default. Swift export itself is opt-in, but within it, Flow export doesn't need to be separately switched on.

What the promotion's baseline actually was is interesting too. The item filed on the Kotlin roadmap under the label "Swift Export: Alpha release" is KT-80305, and the ticket's actual title is "Support coroutines in Swift Export." It was closed as Fixed on March 24, 2026, and landed in 2.4.0-Beta2. In other words, the threshold for Alpha was, in effect, "do coroutines work." Put the other way around, this also means that as long as coroutines didn't work, JetBrains itself judged that this feature could not carry the real API surface of an iOS app.

What "Alpha" Means in JetBrains's Dictionary

Here's where we need to be honest. In many projects Alpha is roughly a marketing word for "coming soon," but Kotlin has this grade defined in its docs. Carrying over the components-stability documentation's definition verbatim:

Alpha means "we are testing whether this should become production-ready." And the explanation that follows is the key part: there's intent to make it a product, and the shape is still being finalized while validating user value and market fit; the feature set is still incomplete and breaking changes are expected; and "we may significantly change or discontinue the feature" if the hypothesis does not hold up.

Comparing this against the definition of the grade right above it, Beta, makes the gap clear. Beta is "you can use it, we'll do our best to minimize migration issues for you" — that carries a promise. Alpha carries no such promise. The Kotlin docs group Experimental, Alpha, and Beta together and call them pre-stable.

And one more sentence the docs add is worth quoting directly — the stability grade says nothing about how quickly that component will ship as Stable. In other words, reaching Alpha does not mean Beta or Stable is anywhere close.

The Swift export documentation itself carries the same tone: "currently in Alpha and still incomplete, so breaking changes are expected."

The currently published Kotlin roadmap shows its last update as February 2026, with the next update scheduled for August 2026. The Swift export item on that roadmap runs only up to the Alpha release, and no item targeting Beta or Stable is listed. Nor does the release notes for 2.4.20-Beta1 (June 24, 2026), the EAP after 2.4.0, mention any Swift export change. Until the next roadmap update, there is no basis in public material to estimate a Beta date. Better to say there isn't one than to invent a date that doesn't exist.

One more thing — JetBrains itself did not heavily promote Swift export in this release. The Kotlin/Native summary line in the 2.4.0 release blog post reads, in full: "Support for Swift packages as dependencies, updates on Swift export, and the CMS GC enabled by default." "Updates on Swift export" — the fact that the Alpha promotion wasn't pulled out as a headline is itself a signal.

What It Still Cannot Do

Gathering the constraints scattered across the docs' "Current limitations" section and its inline notes gives us this list. I've attached how each one actually hurts on a real project.

Direct integration only. This is the biggest wall. Swift export only works in projects that use direct integration to connect their iOS framework into an Xcode project. Per the iOS integration methods documentation, there are five integration paths — direct integration, a local Swift package, local podspec CocoaPods (these three are "local"), and SwiftPM + XCFramework, CocoaPods + XCFramework ("remote"). Swift export supports only one of these. On top of that, direct integration itself is the approach used when a KMP project "does not import a CocoaPods dependency." To sum up:

  • Organizations that package their shared module as an XCFramework and throw it into a separate iOS repo — not applicable.
  • Projects that pull in CocoaPods dependencies on the KMP side — not applicable.
  • A monorepo built with IntelliJ IDEA's KMP plugin or the web wizard — applicable.

This constraint isn't new, either. The same sentence was already in the release notes for Kotlin 2.2.20, when Swift export first shipped by default, and it's still there unchanged in the docs as of 2.4.0, where it reached Alpha. Across three releases, this wall hasn't moved.

Generics. The docs' note here is short and blunt — "Generic types are generally not supported." When exporting to Swift, a Kotlin generic type parameter is type-erased to its upper bound. There's an irony here: as far as generics go, Swift export is not yet better than the Objective-C bridge. Objective-C at least carries lightweight generics over classes, whereas Swift export erases them to the upper bound. In exchange for solving the nullability problem seen in the Sample<T> example earlier, the type parameter itself disappears.

Class constraints. Swift export only supports final classes that directly inherit from Any — things like class Foo(). That means domain models with an inheritance hierarchy, or sealed hierarchies, cannot be exported as-is. An exported class becomes, in Swift, a class that inherits from KotlinRuntime.KotlinBase.

No cross-language inheritance. A Swift class cannot directly subclass a class or interface exported from Kotlin. If you've been using the pattern of defining an interface on the Kotlin side and implementing it on the Swift side, that pattern can't move over as-is.

Collection-inheriting types. Types that inherit List, Set, or Map are ignored during export (KT-80416), and those inheriting types cannot be instantiated on the Swift side (KT-80417). Both issues are unresolved as of July 16, 2026, and their status is Backlog. Backlog means "not scheduled for a release," not "scheduled for the next one." For reference, the two tickets' actual titles are more specific than the docs' summary — they read, respectively, "function with receiver of MutableList is not translated" and "ByteArray is impossible to instantiate." The latter shows that even a common type like ByteArray runs into this.

No IDE support. The docs state it plainly — "No IDE migration tips or automation are available." There's no tooling to help you move off the Objective-C path. You do it by hand.

opt-in friction. If you use a declaration that requires opt-in (e.g. kotlinx.datetime), you have to add an explicit optIn compiler option in a separate block at the module level.

operator restrictions. The docs note that support for functions marked with the operator modifier is currently limited.

The Problem of Int Becoming Int32

This isn't on the constraints list, but if you actually read the mapping table, something jumps out.

KotlinSwift
IntInt32
LongInt64
CharUnicode.UTF16.CodeUnit
AnyKotlinBase class
UnitVoid
NothingNever

Kotlin's Int doesn't map to Swift's Int — it maps to Int32. From a type-correctness standpoint this is accurate — Kotlin's Int really is 32-bit, and Swift's Int is 64-bit on 64-bit platforms, so mapping it to Int would be a lie. Here's what the docs' example produces.

// Kotlin
class MyClass {
    val property: Int = 0
}
// Swift
public class MyClass : KotlinRuntime.KotlinBase {
    public var property: Swift.Int32 { get }
}

Correct, but not idiomatic from the Swift call site's point of view. In Swift code, the default type for handling integers is Int, array indices are Int, and SwiftUI APIs take Int. To use an Int32 coming from Kotlin, you get a conversion at every boundary. Where KotlinInt boxing used to sit, Int32 conversion now sits — a much cheaper trade, no question, but also a sign that the goal of "calling idiomatically from Swift" isn't fully achieved yet. Char becoming Unicode.UTF16.CodeUnit is the same kind of honest inconvenience.

The Rest of 2.4.0's Kotlin/Native Changes

Since we're already looking at Swift export, here's a quick rundown of what else landed in Kotlin/Native in the same release. This part will actually hit your build sooner.

CMS GC is now the default. The concurrent mark-and-sweep (CMS) GC, which arrived experimentally in Kotlin 2.0.20, is the default starting with 2.4.0. The previous default, PMCS (parallel mark concurrent sweep), had to pause application threads while marking objects on the heap, whereas CMS runs its marking phase concurrently with application threads. The release notes state this significantly improves GC pause times and app responsiveness, citing benchmarks on Compose Multiplatform UI apps as evidence — this is JetBrains's own measurement, and the release notes body does not give concrete figures. If you run into trouble, you can revert with kotlin.native.binary.gc=pmcs in gradle.properties.

Apple target minimum versions raised. This is a quiet breaking change. iOS and tvOS went from 14.0 to 15.0, macOS from 11.0 to 12.0, and watchOS from 7.0 to 8.0. If you need to support a lower version, you have to set an option like -Xoverride-konan-properties=minVersion.ios=14.0 in freeCompilerArgs.

LLVM 21. Kotlin/Native's LLVM went from 19 to 21. The release notes state this should not affect your code.

Reduced devirtualization memory. An improvement landed for the problem where the link-release task ate too much memory, especially on very large projects. The release notes' wording needs to be carried over carefully — "according to one EAP user's benchmark," the link-release task's memory consumption was cut in half, saving at least 13GB. That is, it's a self-reported figure from a single user relayed by JetBrains, and the project's scale and environment conditions were not disclosed. There is no guarantee at all that your project will see the same magnitude of improvement.

Swift package import (Experimental). You can now declare a Swift package as a dependency of an iOS app from Gradle configuration. It's written in a swiftPMDependencies block, in the form swiftPackage(url = ..., version = ..., products = ...). The roadmap item (KT-53877) is still Open, and this feature's grade is Experimental, not Alpha.

So, Should You Use It Now

The decision splits like this.

Worth switching on now, if

  • You're starting a new KMP project, it's a direct-integration monorepo setup, and you have no CocoaPods dependency.
  • Your shared module's public API is simple — top-level functions, final data classes, enums, suspend functions, Flow. You are not exporting generic containers or inheritance hierarchies to Swift.
  • You can absorb the Swift boundary continuing to shift. Breaking changes aren't a risk here — they're announced.
  • You're willing to leave feedback. Turning it on at the Alpha stage is effectively casting a vote on the "hypothesis" JetBrains is trying to validate.

Don't turn it on, if

  • You ship your shared module as an XCFramework, or use CocoaPods — this simply isn't possible. That's not a choice, it's a fact.
  • Your public API has generics. They get erased to the upper bound, and this is an area the docs say is "generally not supported."
  • You use the pattern of implementing a Kotlin interface on the Swift side — there's no cross-language inheritance.
  • Your Objective-C path already works and you're on a product timeline. What you'd gain by moving now is syntactic elegance and AsyncSequence; what you'd lose is stability guarantees and IDE support. There's no reason to move a production boundary onto a grade that doesn't even carry Beta's promise yet.

The practical stance is this: Swift export is pointed in the right direction, and with 2.4.0's coroutine and Flow support, it has, for the first time, taken a shape that can carry a real API surface. So cut a branch, switch it on, and count how many times your shared module's public API hits the constraints listed above. That number is both your team's migration readiness and a checklist of what to look for the next time you read a release notes page. Moving a production branch is a separate conversation, and at minimum, one for after Beta.

Closing

The problem Kotlin Multiplatform has long carried on iOS is that there weren't two languages involved — there were three. The Objective-C bridge projected Kotlin's types onto the subset Objective-C could express, and that loss showed up as KotlinInt boxing, generic erasure, and completion handlers. Swift export tears that bridge down. It appeared as Experimental in 2.2.20, picked up enums and varargs in 2.3.0, and reached Alpha in 2.4.0 by gaining coroutines and Flow.

At the same time, there's plenty to look at honestly. By JetBrains's own definition, Alpha is a stage where it's still being tested whether this should become production-ready, and one where the feature could be discontinued if the hypothesis doesn't hold. The direct-integration-only constraint has sat unchanged, sentence for sentence, across three releases; generics are, if anything, worse off than under the old bridge; and there's no IDE support. There's no Beta item on the public roadmap.

So the conclusion lands here: Swift export is not a signal that KMP's iOS story is ending — it's a signal that it's properly beginning. What to do right now isn't migration, it's measurement — count how many times your API runs into that wall, and wait for the August 2026 roadmap update.

References

현재 단락 (1/139)

Kotlin Multiplatform's sales pitch has always been clean: write your business logic once, and Androi...

작성 글자: 0원문 글자: 19,744작성 단락: 0/139