Skip to content
Published on

Kotlin Swift export が Alpha になった — Objective-C の橋を撤去中だが、まだ渡ってはいけない

シェア
Authors

はじめに — KMP が iOS で払う税

Kotlin Multiplatform のセールストークはいつも明快でした。ビジネスロジックを一度書けば、Android と iOS が一緒に使う。Android 側は実際そのとおりになります — 同じ言語、同じツールチェーンだからです。

問題はいつも iOS 側の境界面でした。Kotlin/Native が生成する成果物は、Swift がそのまま読めるものではないからです。Kotlin は Objective-C フレームワークを吐き出し、Swift はその Objective-C ヘッダーを Swift から見える API へと再翻訳して使います。つまり Kotlin と Swift のあいだに、誰も望んでいない第三の言語が挟まっています。どちらもヌル安全性を型に込めたモダンな言語なのに、そのあいだの通訳が1984年生まれの言語というわけです。

この通訳コストは抽象的な話ではありません。以下で見るとおり、型が押しつぶされ、名前が壊れ、コルーチンがコールバックへ逆戻りします。Swift export はこの橋を丸ごと撤去しようとする試みで、Kotlin 2.4.0(2026年6月3日リリース)で Alpha になりました。

Alpha になったこと自体は良いニュースです。ただ、JetBrains の辞書で「Alpha」が正確に何を意味するかを確認すると、いますぐ移行チケットを切る気は失せるはずです。本稿はその両面を整理します。

Objective-C の橋が実際に壊しているもの

まずはなぜこの機能が存在するのかから見る必要があります。Kotlin の Objective-C interop ドキュメント自身が認めている損失です。

プリミティブのボクシング。 ドキュメントの表現をそのまま使えば、kotlin.Int のボックスは Swift で KotlinInt クラスのインスタンスとして表現されます。このクラス群は NSNumber から派生しています。そして逆方向は自動では起きません — ドキュメントは、NSNumber が Swift/Objective-C のパラメーターや戻り値の型として使われるとき、Kotlin のプリミティブへ自動変換されないと明記しています。理由は、NSNumber がラップした値が Byte なのか Boolean なのか Double なのかという情報を静的に持っていないためで、そのため手動でキャストする必要があります。ヌル許容の整数一つを渡すためにヒープ確保されたラッパーが生まれ、Swift 側には手動キャストのコードが生まれます。

ジェネリクスの損失。 Objective-C の「lightweight generics」は Kotlin のジェネリクスを収めるには狭すぎます。ドキュメントが明記する制約は二つです。第一に、ジェネリクスはクラスにしか定義できず、インターフェース(Objective-C・Swift のプロトコル)や関数には定義できません。第二に、ヌル許容性が押しつぶされます。ドキュメントの例をそのまま持ってくると、こういう Kotlin コードが

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

Swift ではこう見えます。

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

TT? に変わりました。生成された Objective-C ヘッダーが myVal をヌル許容として宣言せざるを得ないためです。ドキュメントが勧める回避策は T : Any という上限を直接指定し、ヘッダーが non-null とマークできるようにすることです — つまり、Kotlin 側の API 設計を Objective-C ヘッダーの都合に合わせてねじ曲げろという話になります。

コルーチンがコールバックになる。 Kotlin の suspend 関数は、生成された Objective-C ヘッダーではコールバックを受け取る関数、Swift の用語ではコンプリーションハンドラーとして現れます。Swift 5.5 から async 関数として呼べる道が一応開かれましたが、ドキュメントはこの機能が「highly experimental」であり特定の制約があると書いており、詳細は KT-47610 を見よとしています。本稿執筆時点である2026年7月16日現在、この issue は依然として Open です。サブシステムは Language Design と Native。ObjC Export のタグが付いています。何年も開いたままのこの issue が、Swift export という別経路が必要だった理由を凝縮して示しています。

名前の崩壊。 Swift export のドキュメントが自ら掲げる動機がこれです — Objective-C の紛らわしいアンダースコアと崩れた名前(mangled names)をなくすこと。トップレベル関数がどこかの SharedKt のようなクラスに付いて現れるのを見たことがあれば、何の話か分かるはずです。

まとめると、Objective-C の橋は Kotlin の API をそのまま運べず、Objective-C が表現できる部分集合へ投影します。 その損失は設計上避けられないものでした。

Swift export がやっていること

Swift export の発想は単純です。中間言語を取り除くこと。

公式ドキュメントの最初の一文が的確です — Swift export は Kotlin のソースを直接エクスポートして Swift から慣用的に Kotlin コードを呼べるようにし、Objective-C ヘッダーをなくす。コンパイラーが swiftmodule ファイル、静的 .a ライブラリ、ヘッダーファイル、modulemap ファイルを直接作り、アプリのビルドディレクトリへコピーします。

設定は Gradle 側のブロック一つです。

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

    swiftExport {
        // ルートモジュール名
        moduleName = "Shared"

        // 生成される Swift コードからパッケージ接頭辞を除去
        flattenPackage = "com.example.sandbox"

        // 外部モジュールの export 設定
        export(project(":subproject")) {
            moduleName = "Subproject"
            flattenPackage = "com.subproject.library"
        }
    }
}

そして Xcode の Build Phases で、Run Script のタスクを embedAndSignAppleFrameworkForXcode から embedSwiftExportForXcode に切り替えます。ドキュメントが提示するスクリプトは ./gradlew :<モジュール名>:embedSwiftExportForXcode という形です。

前節で見た損失のうちいくつかは実際になくなります。ドキュメントが挙げているものです。

  • プリミティブのヌル許容性が改善。 Int? のような型を KotlinInt ラッパーへボクシングする必要があった Objective-C interop と違い、Swift export はヌル許容性の情報を直接変換します。
  • マルチモジュール対応。 Kotlin モジュール一つが Swift モジュール一つとして出ていきます。
  • パッケージの保存。 Kotlin のパッケージが明示的に保存され、名前の衝突を避けます(ネストした Swift enum に翻訳されます)。
  • オーバーロード。 Kotlin のオーバーロードされた関数を Swift から曖昧さなく呼べます。
  • 型エイリアス。 typealias がそのまま Swift に残ります。

さらに Kotlin 2.3.0(2025年12月16日)で二つ追加されました。Kotlin の enum class が以前のような普通の Swift クラスとしてではなく、本物のネイティブ Swift enum へマッピングされるようになり、vararg 関数が Swift の可変長パラメーターへ直接マッピングされるようになりました。

2.4.0 が変えたこと — 昇格、そしてコルーチン

Kotlin 2.4.0 リリースノートが語る今回の変化の核心は二つです。

第一に、構造化された並行性。 Kotlin の suspend 関数と suspend 関数型が、Swift の慣用的な async として出ていきます。コンプリーションハンドラーとしてではなく。

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

第二に、Flow のエクスポート。 こちらのほうが実務的には大きい変化です。ドキュメントが正確に指摘するとおり、これまで kotlinx.coroutines.flowFlow インターフェースを Swift に公開する唯一の方法はサードパーティ製ソリューションでした。いまでは Flow が Swift の AsyncSequence としてデフォルトで有効になった状態で出ていき、型情報も保存されます。

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

// String 型が Kotlin から正しく推論される
for try await element in flowOfStrings().asAsyncSequence() {
    actual.append(element)
}

KMP を実際に運用したことがある人なら、この一行の重みが分かるはずです。「Kotlin 側の状態ストリームを SwiftUI にどう繋ぐか」は KMP を導入するチームが必ずぶつかる問いで、これまでの答えは常にサードパーティ製ラッパーでした。

デフォルトで有効という点も付記に値します。Swift export そのものは opt-in ですが、その中で Flow export は別途オンにする必要がありません。

昇格の基準線が何だったのかも興味深いところです。Kotlin ロードマップに「Swift Export: Alpha release」というラベルで掛かっていた項目は KT-80305 で、チケットの実際のタイトルは "Support coroutines in Swift Export" です。2026年3月24日に Fixed としてクローズされ、2.4.0-Beta2 に入りました。つまり Alpha の関門は事実上「コルーチンが動くかどうか」でした。 裏を返せば、コルーチンが動かないあいだは、この機能が iOS アプリの実際の API 表面を支えられないと JetBrains 自身が判断していたということでもあります。

JetBrains の辞書で「Alpha」が意味すること

ここで正直になるべき部分です。多くのプロジェクトでは Alpha は「もうすぐです」程度のマーケティング用語ですが、Kotlin はこの等級をドキュメントで定義しています。components-stability ドキュメントの定義をそのまま持ってくるとこうなります。

Alpha とは "we are testing whether this should become production-ready" — これをプロダクションレディにすべきかを検証している最中という意味です。そして続く説明が要点です。製品化する意図があり、ユーザー価値と市場適合性を検証しながら最終形を定めている途中であり、機能集合はまだ不完全で破壊的変更が見込まれ、仮説が成立しなければ機能を大きく変更するか中止することがある("we may significantly change or discontinue the feature")。

すぐ下の等級である Beta の定義と比べると、その差は鮮明です。Beta は "you can use it, we'll do our best to minimize migration issues for you" — 使ってよい、移行問題を最小化するために最善を尽くすという約束が入っています。Alpha にはその約束がありません。Kotlin のドキュメントは Experimental・Alpha・Beta をまとめて pre-stable と呼んでいます。

そしてドキュメントが付け加える一文もそのまま引く価値があります — 安定性の等級は、そのコンポーネントがどれだけ早く Stable としてリリースされるかについては何も語らない。つまり Alpha になったからといって、Beta や Stable が近いという意味には全くなりません。

Swift export のドキュメント本文も同じトーンです。"currently in Alpha and still incomplete, so breaking changes are expected."

現在公開されている Kotlin ロードマップは、最終更新が2026年2月、次回更新予定が2026年8月と記されています。そのロードマップに掛かっている Swift export の項目は Alpha リリースまでで、Beta や Stable を狙った項目は載っていません。2.4.0 の次の EAP である 2.4.20-Beta1(2026年6月24日)のリリースノートにも Swift export 関連の変更はありません。次のロードマップ更新までは、公開資料だけで Beta の時期を推定する根拠はありません。ない日付をでっち上げるより、ないと言うほうがましです。

もう一つ — JetBrains 自身も今回のリリースで Swift export を大きく宣伝してはいません。2.4.0 リリースブログの Kotlin/Native まとめの一文は "Support for Swift packages as dependencies, updates on Swift export, and the CMS GC enabled by default" がすべてです。"updates on Swift export" — Alpha 昇格を見出しに引っ張り出さなかったこと自体が、一つのシグナルです。

まだできていないこと

ドキュメントの "Current limitations" 節と本文中の注釈に散らばる制約を集めると、こうなります。それぞれが実プロジェクトでどう痛むかを添えておきます。

direct integration 専用。 これが最大の壁です。Swift export は、iOS フレームワークを Xcode プロジェクトに繋ぐときに direct integration を使うプロジェクトでしか動きません。iOS 統合方式のドキュメントを見ると、統合経路は五つに分かれます — direct integration、ローカル Swift パッケージ、ローカル podspec CocoaPods(ここまでが local)、そして SwiftPM + XCFramework、CocoaPods + XCFramework(remote)。Swift export はこのうち一つしかサポートしません。しかも direct integration そのものが「KMP プロジェクトに CocoaPods 依存関係を import しない場合」に使う方式です。整理するとこうなります。

  • 共有モジュールを XCFramework にまとめて別の iOS リポジトリに投げ込む組織 — 該当しない。
  • KMP 側で CocoaPods 依存関係を引いているプロジェクト — 該当しない。
  • IntelliJ IDEA の KMP プラグインや Web ウィザードで作ったモノレポ — 該当する。

この制約は新しいものでもありません。Swift export が最初にデフォルトで提供された Kotlin 2.2.20 のリリースノートに、すでに同じ文が書かれていましたし、Alpha になった 2.4.0 時点のドキュメントにもそのまま残っています。三つのリリースを渡り歩くあいだ、この壁は動いていません。

ジェネリクス。 ドキュメントの注釈は短く断定的です — "Generic types are generally not supported."。Swift へエクスポートするとき、Kotlin のジェネリック型パラメーターは上限(upper bound)で型消去されます。ここで一つ皮肉が生まれます。ジェネリクスに関する限り、Swift export はまだ Objective-C の橋より優れているわけではありません。 Objective-C は少なくともクラスの上では lightweight generics を運びますが、Swift export は上限で消し去ってしまうからです。前節で見た Sample<T> の例のヌル許容性の問題が解決される代わりに、型パラメーター自体が消えるわけです。

クラス制約。 Swift export は Any を直接継承する final クラスしかサポートしません。class Foo() のようなものです。つまり継承階層を持つドメインモデルや sealed 階層はそのままエクスポートできません。エクスポートされたクラスは Swift 側で KotlinRuntime.KotlinBase を継承したクラスになります。

クロス言語の継承不可。 Swift クラスが、Kotlin からエクスポートされたクラスやインターフェースを直接サブクラス化することはできません。Kotlin 側にインターフェースを定義して Swift 側で実装して渡す、というパターンを使っていたなら、そのままは移せません。

コレクション継承型。 ListSetMap を継承する型は export の過程で無視され(KT-80416)、その継承型は Swift 側でインスタンス化できません(KT-80417)。両方の issue とも2026年7月16日現在未解決で、ステータスは Backlog です。Backlog は「次のリリースに予定されている」ではなく「予定されていない」という意味です。参考までに、両チケットの実際のタイトルはドキュメントの要約より具体的です — それぞれ "function with receiver of MutableList is not translated" と "ByteArray is impossible to instantiate" です。後者を見ると、ByteArray のような一般的な型さえ引っかかることが分かります。

IDE サポートなし。 ドキュメントが明記しています — "No IDE migration tips or automation are available."。Objective-C 経路から移るのを助けるツールはありません。手で行います。

opt-in の煩わしさ。 opt-in が必要な宣言(たとえば kotlinx.datetime)を使う場合、モジュールレベルで明示的な optIn コンパイラーオプションを別ブロックとして追加する必要があります。

operator の制限。 operator 修飾子が付いた関数のサポートは現時点で限定的だとドキュメントに書かれています。

Int が Int32 になる問題

制約リストには入っていませんが、マッピング表を実際に読むと目に留まるものがあります。

KotlinSwift
IntInt32
LongInt64
CharUnicode.UTF16.CodeUnit
AnyKotlinBase クラス
UnitVoid
NothingNever

Kotlin の Int は Swift の Int ではなく Int32 へ行きます。型の観点では正確です — Kotlin の Int は確かに32ビットで、Swift の Int は64ビットプラットフォームでは64ビットなので、これを Int へマッピングすると嘘になります。ドキュメントの例が示す結果はこうです。

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

正確ではありますが、Swift 側の呼び出し元にとっては慣用的ではありません。Swift コードで整数を扱う基本型は Int であり、配列のインデックスも Int であり、SwiftUI の API も Int を受け取ります。Kotlin から来た Int32 を使うには、境界のたびに変換が挟まります。KotlinInt のボクシングが消えた場所に Int32 の変換が入ってきたわけです — 明らかにずっと安い取引ですが、「Swift から慣用的に呼ぶ」という目標がまだ完全には達成されていないことの表れでもあります。CharUnicode.UTF16.CodeUnit になるのも同種の正直な不便さです。

2.4.0 のその他の Kotlin/Native 変更点

Swift export を見たついでに、同じリリースで Kotlin/Native に入った他の変更も短く整理しておきます。こちらのほうが、いま皆さんのビルドに直接効いてきます。

CMS GC がデフォルトになった。 Kotlin 2.0.20 で実験的に入った concurrent mark-and-sweep(CMS)GC が、2.4.0からデフォルトになりました。それまでデフォルトだった PMCS(parallel mark concurrent sweep)は、ヒープ上のオブジェクトをマーキングするあいだアプリケーションスレッドを止める必要がありましたが、CMS はマーキング段階をアプリケーションスレッドと同時に走らせます。リリースノートはこれが GC の一時停止時間とアプリの応答性を大きく改善すると書いており、根拠として Compose Multiplatform UI アプリのベンチマークを挙げます — これは JetBrains 自身の測定で、リリースノート本文には具体的な数値は示されていません。問題が起きた場合は gradle.propertieskotlin.native.binary.gc=pmcs と書いて元に戻せます。

Apple ターゲットの最低バージョン引き上げ。 これは静かな破壊的変更です。iOS と tvOS が14.0から15.0へ、macOS が11.0から12.0へ、watchOS が7.0から8.0へ上がりました。より低いバージョンをサポートする必要がある場合は、freeCompilerArgs-Xoverride-konan-properties=minVersion.ios=14.0 のようなオプションを指定する必要があります。

LLVM 21。 Kotlin/Native の LLVM が19から21へ上がりました。リリースノートはコードへの影響はないはずだと書いています。

devirtualization のメモリ削減。 リンクリリースタスクが、特に大規模プロジェクトでメモリを食いすぎていた問題に改善が入りました。リリースノートの表現は慎重に持ち込む必要があります — "EAP ユーザーの一人によるベンチマークによれば" リンクリリースタスクのメモリ消費が半分になり、最低でも13GB節約できたとされています。つまり JetBrains が伝えたユーザー一人分の自己測定値であり、プロジェクトの規模や環境条件は公開されていません。皆さんのプロジェクトで同じ幅が出る保証はまったくありません。

Swift パッケージのインポート(Experimental)。 Gradle の設定から、Swift パッケージを iOS アプリの依存関係として宣言できるようになりました。swiftPMDependencies ブロックに swiftPackage(url = ..., version = ..., products = ...) という形で書きます。ロードマップの項目(KT-53877)はまだ Open で、この機能の等級は Alpha ではなく Experimental です。

で、いま使うべきか

判断はこう分かれます。

いま試す価値がある場合

  • 新規で始める KMP プロジェクトで、direct integration のモノレポ構成で、CocoaPods 依存関係がない。
  • 共有モジュールの公開 API がシンプル — トップレベル関数、final なデータクラス、enum、suspend 関数、Flow。ジェネリックなコンテナや継承階層を Swift へエクスポートしない。
  • Swift の境界面が変わり続けても耐えられる。破壊的変更は予想されるものではなく、予告されているものです。
  • フィードバックを残す意思がある。Alpha 段階で JetBrains が検証しようとしている「仮説」に一票を投じることになります。

オンにしてはいけない場合

  • 共有モジュールを XCFramework として配布している、あるいは CocoaPods を使っている — そもそもできません。選択肢ではなく事実です。
  • 公開 API にジェネリクスがある。上限で消去されますし、ドキュメントが「一般的にサポートされていない」と明記している領域です。
  • Swift 側で Kotlin のインターフェースを実装するパターンを使っている — クロス言語の継承がありません。
  • すでに Objective-C 経路が動いていて、製品スケジュールがある。いま移して得られるのは構文上の美しさと AsyncSequence で、失うのは安定性の保証と IDE サポートです。Beta の約束すらまだない等級に、プロダクションの境界面を移す理由はありません。

現実的な姿勢はこうです。Swift export は方向として正しく、2.4.0 のコルーチン・Flow サポートで初めて「実際の API 表面を支えられる」形になりました。だからブランチを一つ切って試しに入れてみて、皆さんの共有モジュールの公開 API があの制約リストに何回引っかかるかを数えてください。 その数字が、皆さんのチームの移行準備度であり、次のリリースノートを読むときに何を探すべきかを教えるチェックリストです。プロダクションブランチを移すのはその先の話で、少なくとも Beta 以降の話です。

おわりに

Kotlin Multiplatform が iOS で長く抱えてきた問題は、言語が二つではなく三つだったということです。Objective-C の橋は Kotlin の型を Objective-C が表現可能な部分集合へ投影し、その損失が KotlinInt のボクシング、ジェネリクスの消去、コンプリーションハンドラーとして現れました。Swift export はその橋を撤去します。2.2.20 で Experimental として登場し、2.3.0 で enum と vararg を取り込み、2.4.0 でコルーチンと Flow を得て Alpha になりました。

同時に、正直に見るべきこともはっきりしています。JetBrains 自身の定義では、Alpha はプロダクションレディになるべきかをまだ検証している段階であり、仮説が外れれば中止されうる段階です。direct integration 専用という制約は三つのリリースのあいだ一文も変わらずに残っており、ジェネリクスはむしろ古い橋より劣っており、IDE サポートはありません。公開ロードマップには Beta の項目がありません。

そのため結論はこう整理されます。Swift export は KMP の iOS ストーリーが終わるという合図ではなく、ようやく本格的に始まったという合図です。いまやるべきは移行ではなく計測です — 皆さんの API があの壁に何回ぶつかるかを数えておき、2026年8月のロードマップ更新を待ってください。

参考資料