はじめに
アイスタイル Advent Calendar 2025の22日目を担当させて頂きますishimarutです。
2025年5月に入社した時点で、アプリチームではすでにKotlin Multiplatform(KMP)がバリバリ活用されていました。
今回は、KMP未経験iOSエンジニアが、実際に業務でKMPに触れて「はまった!」という事例を6つ紹介します。
はまった事例6選
1. KMM?KMP?CMP?用語の理解があいまい
◾️
落とし穴
KMMが古いことを知らない。「KMP」と「CMP」の用語の使い分けに戸惑う。
◾️
攻略の鍵
歴史的経緯により用語が変わっています。「KMM」は2023年に「KMP」へ名称変更されており、現在は推奨されません。
その上で、以下の表のように脳内を整理するのがおすすめです。
| 用語 | 正式名称 | 役割 |
|---|---|---|
| KMP | Kotlin Multiplatform | 土台(ロジック共有) |
| CMP | Compose Multiplatform | UIフレームワーク |
| KMM | Kotlin Multiplatform Mobile | KMPの旧名称 |
「UIを指すならCMP、ロジック共有部分のみを指すならKMP、UIとロジック両方ひっくるめるならKMP」 と覚えましょう。
またCMPを使うためには、必ずKMPという土台が必要です。
文脈による使い分け例:
- 「KMP導入してる?」と聞かれた時、UI(CMP)を使っていても使っていなくても「Yes」になります
- 「UIはどうしてる?」と聞かれた時に初めて「CMPを使ってる」という区別が必要になります
2. CMPのViewの参照の仕方がわからない
◾️
落とし穴
KMP側(commonMain)で書いたUI(@Composable関数)を、Swift側でそのまま View や Class として呼び出せると思い込んでしまう。
「あれ? Xcodeの補完にViewが出てこない?」と焦る。
◾️
攻略の鍵
KMP側の @Composable は、そのままではSwiftから見えません。
iosMain において ComposeUIViewController でラップして初めてSwift側で利用可能になります。
具体的なコード例:
Step 1: 共通コード(commonMain)でUIを定義
// commonMain/src/commonMain/kotlin/App.kt
@Composable
fun App() {
Text("Hello, KMP World!")
}
Step 2: iOS向けコード(iosMain)でViewController化
// commonMain/src/iosMain/kotlin/MainViewController.kt
import androidx.compose.ui.window.ComposeUIViewController
// ここでSwiftから呼べる形にラップする
fun MainViewController() = ComposeUIViewController {
App()
}
Step 3: Swift側で呼び出し
// iOSApp/ContentView.swift
import SwiftUI
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
// KMP側で定義したMainViewControllerを呼び出す
return MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
【補足】commonMainとiosMain って何?
-
commonMain- 役割: 全プラットフォーム共有のコードを書く場所。
-
特徴:
UIKitやFoundationなど、iOS固有の機能は使用できない。
-
iosMain- 役割: iOS固有のコードを書く場所。
-
特徴:
UIKitやFoundationが使える。また、commonMainの中身も見える。
今回のケース:
CMPのUI(@Composable)は commonMain に書かれますが、それはただの「Kotlinの関数」であり、iOSの UIView や、UIViewController として認識されません。
そこで、iOSの機能にアクセスできる iosMain で、UIViewControllerとして利用できるよう変換しています。
3. メソッドのデフォルト引数が使えない
◾️
落とし穴
KMP側で便利に使っている「デフォルト引数」が、Swift側で機能しない。
例 fun search(keyword: String, limit: Int = 10) と定義しても、Swiftから呼び出す際は limit を省略できず、エラーになる。
◾️
攻略の鍵
「SwiftはKotlinのデフォルト引数を理解できない(Objective-C経由のため)」 という仕様を認識しましょう。
対策:
以下のいずれかで対応します。
- デフォルト引数を使わない: 設計として必須引数のみにする
- オーバーロード関数を用意する: Kotlin側で引数が少ないバージョンを別途定義する
具体的なコード例:
class SearchRepository {
// 1. 本来の関数(Kotlinからはlimit省略可能)
fun search(keyword: String, limit: Int = 10): List<String> {
// ... 検索ロジック ...
}
// 2. iOS用にオーバーロードを手動定義
fun search(keyword: String): List<String> {
// ... 検索ロジック ...
}
}
// Swift側
let repository = SearchRepository()
// オーバーロード関数を用意したおかげで、limitなしで呼べる
let results = repository.search(keyword: "Kotlin")
// もちろん、limitを指定する本来の関数も呼べる
let moreResults = repository.search(keyword: "Kotlin", limit: 50)
4. ステータスバータップでスクロールリセットが効かない
◾️
落とし穴
CMPで実装したスクロール可能なリスト。iOS標準アプリならステータスバーをタップすると一番上に戻るはずが、うんともすんとも言わない。
◾️
攻略の鍵
CMPのリスト(LazyColumnなど)は、iOS側から見ると UIScrollView のインスタンスではありません。単なる「描画されたView」に過ぎないため、ステータスバータップ時のiOSからの「一番上に戻れ」というイベントは無視されてしまいます。
この修正は意外と面倒です。
単に scrollsToTop = true のような設定をするだけでは直りません。
- iOS側でダミーのスクロールビューを配置して、ステータスバーのタップイベントを検知する
- それをKotlin側に伝播させる
- Kotlin側で
listState.animateScrollToItem(0)を呼ぶ
という「自前実装」が必要です。
◾️実装例
JetBrains公式の KotlinConf App の実装が非常に参考になります。
-
iosMain側
ScrollToTopHandler.kt (iosMain)-
ポイント: 見えないダミーの
UIScrollViewを仕込んで、scrollToTopを有効化。ステータスバーのタップイベントを検知したら 、scrollState.animateScrollToItem(0)を呼び出しスクロール位置をリセットしています。
-
ポイント: 見えないダミーの
-
commonMain側
ScrollToTopHandler.kt (commonMain)- ポイント: iOS側で処理をするためのインターフェースが定義されています。
-
リストでの利用
ScheduleScreen.kt-
ポイント:
ScrollToTopHandlerにLazyListStateを渡すだけの処理になっています。
-
ポイント:
◾️おまけ:スクロールバーも出ない
CMPのリスト(LazyColumnなど)はデフォルトでスクロールインジケータ(右側のバー)を表示しないので注意。
5. 例外が飛んでこない
◾️
落とし穴
KMP側で throw Exception("Error!") と書いたのに、Swift側の do-catch 構文でキャッチできず、アプリがそのままクラッシュ してしまう。
◾️
攻略の鍵
Kotlinの例外をSwiftで受け取るには、@Throws アノテーション が必須です。
対策:
// これがないとSwift側で try-catch できない
@Throws(Exception::class)
fun riskyOperation() {
throw Exception("Something went wrong")
}
// Swift側
do {
try kmpObject.riskyOperation()
} catch {
print("error: \(error)") // これでキャッチできる
}
6. テーブルのセルにCMPのViewを組み込むと表示がおかしくなる
◾️
落とし穴
既存のiOSアプリのUITableView のセルの中にCMPのViewを表示しようとすると、高さ計算がおかしくなったり、スクロール時に表示が崩れたりする。
◾️
攻略の鍵
「現時点ではUITableViewのセルとCMPの相性は悪い」 と認識しておきましょう。
セルの再利用(Reuse)や、AutoLayoutとComposeのレイアウトシステムの協調が難しいためです。
現状のベストプラクティスとしては、セル単位のような細かい粒度ではなく、「画面全体」または「大きなコンポーネント単位」でCMPを利用するのが安全です。
弊社メンバーの記事でも紹介されております。ぜひこちらも合わせてご参照ください。
Compose MultiplatformをUITableViewCellに組み込んでハマった話
おわりに
いかがでしたでしょうか。
上記のようなハマりどころはありましたが、それを乗り越えればロジック共有の恩恵は絶大です。
この記事が、これからKMPに挑戦されるiOSエンジニアの皆さまの助けになれば幸いです。