KMPのマルチモジュール構成では、StaticなiOSフレームワークは1つに集約すべきっぽい

この記事は、筆者のメモを基に、AIが構成・加筆を行ったものです。

KMPのプロジェクトで複数のモジュールを作成し、それぞれをiOS側で使えるようにしていました。build-logicを以下のように作成して、複数のモジュールから参照していたのですが、Skieを導入したタイミングで問題が発生しました。

```kotlin
import co.touchlab.skie.plugin.configuration.skieExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

open class KmpPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            plugins.apply("org.jetbrains.kotlin.multiplatform")
            plugins.apply("com.android.library")
            plugins.apply("co.touchlab.skie")
            extensions.getByType(KotlinMultiplatformExtension::class).apply {
                androidTarget {
                    compilerOptions {
                        jvmTarget.set(JvmTarget.JVM_21)
                    }
                }

                listOf(
                    iosX64(),
                    iosArm64(),
                    iosSimulatorArm64(),
                ).forEach { iosTarget ->
                    iosTarget.binaries.framework {
                        baseName = "iOSKmp${project.name}"
                        isStatic = true
                    }
                }

                sourceSets.apply {
                    commonMain.dependencies {
                    }
                    commonTest.dependencies {
                        implementation(libs.findLibrary("kotlin-test").get())
                    }
                }
            }
            configureAndroidLibrary(this)
            skieExtension.apply {
                features {
                    enableSwiftUIObservingPreview.set(true)
                }
            }
        }
    }
}
```

アプリを実行すると、以下のようなエラーが発生しました：

```
objc[501]: Class IOSKSkieColdFlowIterator is implemented in both /private/var/containers/Bundle/Application/9E7BBDB1-E633-463F-AA33-552FFFAE156B/KotlinProject.app/KotlinProject.debug.dylib (0x10b479e30) and /private/var/containers/Bundle/Application/9E7BBDB1-E633-463F-AA33-552FFFAE156B/KotlinProject.app/KotlinProject.debug.dylib (0x10b479e30). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[501]: Class IOSKSkieKotlinFlow is implemented in both
...
```

どうやら、複数のモジュールからそれぞれStaticなフレームワークを生成していることが原因のよう。

Kotlinの公式ドキュメントを確認してみると、実はこの問題について言及されていました：

> For example, you implement several modules in Kotlin and want to access them from Swift. Usage of several Kotlin/Native frameworks in a Swift application is limited, but you can create an umbrella framework and export all these modules to it.

https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#export-dependencies-to-binaries

つまり、複数のKotlinモジュールをSwiftから利用する場合は、「アンブレラフレームワーク」として1つにまとめるべきということです。

## 試したこと

### 1. キャッシュを無効にする

まず、以下の設定を試してみました：

```kotlin
kotlin.native.cacheKind=none
```

これはYouTrackのissue（https://youtrack.jetbrains.com/issue/KT-42254）で提案されていた方法ですが、残念ながら私の環境では解決しませんでした。また、この方法はパフォーマンスにも悪影響があるようです。

### 2. 動的フレームワークに変更する

次に、静的フレームワークではなく動的フレームワークを使用する方法を検討しました：

```kotlin
isStatic = false
```

これは動作する可能性がありますが、アプリ申請時にはXcodeのBuild Phasesで「Embed & Sign」を正しく設定する必要があり、別の問題が発生する可能性があります。

### 3. アンブレラフレームワークを作成する（推奨解決策）

最終的に採用したのは、iOS用のエントリーポイントとなる専用モジュールを1つ作成し、そこから必要なモジュールをエクスポートする方法です。

以下のように`ios-umbrella`モジュールを作成しました：

```kotlin
plugins {
    id("composit.build.kmp")
} 

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64(),
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "SharedIOSFramework"
            isStatic = true
        }
    }

    sourceSets.commonMain.dependencies {
        api(projects.domain)
        api(projects.common)
        // 他の必要なモジュール
    }
}
```

この方法で、iOS側に公開するフレームワークを1つにまとめることができました。

## 注意点

- `commonMain`にKotlinソースがないとコンパイラが反応しないため、ダミーの`.kt`ファイルも必要です