5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iOSエンジニアが知るべきKotlin Multiplatform(KMP)のハマりどころ 6選

Last updated at Posted at 2025-12-11

はじめに

アイスタイル Advent Calendar 2025の22日目を担当させて頂きますishimarutです。
2025年5月に入社した時点で、アプリチームではすでにKotlin Multiplatform(KMP)がバリバリ活用されていました。
今回は、KMP未経験iOSエンジニアが、実際に業務でKMPに触れて「はまった!」という事例を6つ紹介します。

はまった事例6選

1. KMM?KMP?CMP?用語の理解があいまい

◾️:x: 落とし穴

KMMが古いことを知らない。「KMP」と「CMP」の用語の使い分けに戸惑う。

◾️:key: 攻略の鍵

歴史的経緯により用語が変わっています。「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の参照の仕方がわからない

◾️:x: 落とし穴

KMP側(commonMain)で書いたUI(@Composable関数)を、Swift側でそのまま ViewClass として呼び出せると思い込んでしまう。

「あれ? Xcodeの補完にViewが出てこない?」と焦る。

◾️:key: 攻略の鍵

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) {}
}

【補足】commonMainiosMain って何?

  • commonMain

    • 役割: 全プラットフォーム共有のコードを書く場所。
    • 特徴: UIKitFoundation など、iOS固有の機能は使用できない。
  • iosMain

    • 役割: iOS固有のコードを書く場所。
    • 特徴: UIKitFoundation が使える。また、commonMain の中身も見える。

今回のケース:

CMPのUI(@Composable)は commonMain に書かれますが、それはただの「Kotlinの関数」であり、iOSの UIView や、UIViewController として認識されません。

そこで、iOSの機能にアクセスできる iosMain で、UIViewControllerとして利用できるよう変換しています。

3. メソッドのデフォルト引数が使えない

◾️:x: 落とし穴

KMP側で便利に使っている「デフォルト引数」が、Swift側で機能しない。

例 fun search(keyword: String, limit: Int = 10) と定義しても、Swiftから呼び出す際は limit を省略できず、エラーになる。

◾️:key: 攻略の鍵

「SwiftはKotlinのデフォルト引数を理解できない(Objective-C経由のため)」 という仕様を認識しましょう。

対策:
以下のいずれかで対応します。

  1. デフォルト引数を使わない: 設計として必須引数のみにする
  2. オーバーロード関数を用意する: 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. ステータスバータップでスクロールリセットが効かない

◾️:x: 落とし穴

CMPで実装したスクロール可能なリスト。iOS標準アプリならステータスバーをタップすると一番上に戻るはずが、うんともすんとも言わない。

◾️:key: 攻略の鍵

CMPのリスト(LazyColumnなど)は、iOS側から見ると UIScrollView のインスタンスではありません。単なる「描画されたView」に過ぎないため、ステータスバータップ時のiOSからの「一番上に戻れ」というイベントは無視されてしまいます。

この修正は意外と面倒です。
単に scrollsToTop = true のような設定をするだけでは直りません。

  1. iOS側でダミーのスクロールビューを配置して、ステータスバーのタップイベントを検知する
  2. それをKotlin側に伝播させる
  3. Kotlin側で listState.animateScrollToItem(0) を呼ぶ

という「自前実装」が必要です。

◾️実装例

JetBrains公式の KotlinConf App の実装が非常に参考になります。

  1. iosMain
    ScrollToTopHandler.kt (iosMain)

    • ポイント: 見えないダミーの UIScrollView を仕込んで、scrollToTop を有効化。ステータスバーのタップイベントを検知したら 、scrollState.animateScrollToItem(0) を呼び出しスクロール位置をリセットしています。
  2. commonMain
    ScrollToTopHandler.kt (commonMain)

    • ポイント: iOS側で処理をするためのインターフェースが定義されています。
  3. リストでの利用
    ScheduleScreen.kt

    • ポイント: ScrollToTopHandlerLazyListStateを渡すだけの処理になっています。

◾️おまけ:スクロールバーも出ない

CMPのリスト(LazyColumnなど)はデフォルトでスクロールインジケータ(右側のバー)を表示しないので注意。

5. 例外が飛んでこない

◾️:x: 落とし穴

KMP側で throw Exception("Error!") と書いたのに、Swift側の do-catch 構文でキャッチできず、アプリがそのままクラッシュ してしまう。

◾️:key: 攻略の鍵

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を組み込むと表示がおかしくなる

◾️:x: 落とし穴

既存のiOSアプリのUITableView のセルの中にCMPのViewを表示しようとすると、高さ計算がおかしくなったり、スクロール時に表示が崩れたりする。

◾️:key: 攻略の鍵

「現時点ではUITableViewのセルとCMPの相性は悪い」 と認識しておきましょう。
セルの再利用(Reuse)や、AutoLayoutとComposeのレイアウトシステムの協調が難しいためです。

現状のベストプラクティスとしては、セル単位のような細かい粒度ではなく、「画面全体」または「大きなコンポーネント単位」でCMPを利用するのが安全です。

弊社メンバーの記事でも紹介されております。ぜひこちらも合わせてご参照ください。
Compose MultiplatformをUITableViewCellに組み込んでハマった話

おわりに

いかがでしたでしょうか。
上記のようなハマりどころはありましたが、それを乗り越えればロジック共有の恩恵は絶大です。
この記事が、これからKMPに挑戦されるiOSエンジニアの皆さまの助けになれば幸いです。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?