Kotlin Multiplatformを用いたプロジェクトでiOSアプリをマルチモジュール構成にする

## はじめに
本田技研工業でiOSアプリ開発を担当している薄田です。
現在、弊社ではモバイルアプリを開発するにあたり、KotlinMultiplatform(以下KMP)の導入を検討しています。
KMPを使えば、Kotlinで書かれたコードを複数のプラットフォーム(iOS/Android/Web等)で共有することができます。これによりロジック実装の重複を避けることができ、開発効率の向上が見込めます。

最近はマルチモジュール構成のiOSプロジェクトをよく見かけるため、KMPプロジェクトでも同様の構成でiOSアプリを作成することができないか気になり調べてみました。
本記事ではKMPプロジェクトでiOSアプリをマルチモジュール構成にする方法を紹介したいと思います。

## マルチモジュール構成のメリット
少しだけマルチモジュール構成のメリットに関して触れようと思います。
マルチモジュール構成を採用することで、以下のようなメリットを得ることができます。
- モジュール単位でビルド可能なため、Xcodeプレビューやテストを実行する際のビルド時間を短縮することができる
- いくつかのモジュールを組み合わせて機能単体で動くミニアプリを構築できるため、開発サイクルが高速に回せる
- 機能ごとにモジュールを分割することで、モジュール間の依存関係が明確になる

## KMPプロジェクトでマルチモジュール構成にする
### KMPプロジェクトの作成
まずはAndroidStudioからKMPプロジェクトを作成します。
このとき、iOS framweork distributionはRegular frameworkを選択してください。
![スクリーンショット 2024-03-27 22.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/222900b7-caf2-8f8d-6267-8f7f9dc92110.png)

### iOSプロジェクトを開く
KMPプロジェクトを作成すると、.xcodeprojファイルも自動で生成されます。
``/入力したApp名/入力したApp名.xcodeproj``にあるため、iosApp名をデフォルトの状態で作成した場合は``/iosApp/iosApp.xcodeproj``となります。
このプロジェクトファイルを開いてください。

### SwiftPackageの追加
次にSwiftPackageを追加していきます。
ツールバーのFile > New > Package...を選択してください。
![スクリーンショット 2024-03-27 22.39.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/8d40bf70-e3a3-ef90-d0b5-8f15306df940.png)

Libraryを選択し、
![スクリーンショット 2024-03-27 22.42.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/0dd4e74d-6813-84d0-212b-aa9630944b31.png)

Add to, Groupには対象のプロジェクトを選択し、作成してください。
![スクリーンショット 2024-03-27 22.44.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/31492fa9-183c-e947-ed91-e942ba9a4210.png)

このような構成になっているかと思います。
![スクリーンショット 2024-03-28 0.48.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/30fdb6a1-c0d1-7bf5-6c6a-e37d5a9af787.png)


### 共有ロジックをXCFramework形式で生成する
KMPでは、Kotlin/Nativeコンパイラが各プラットフォームに合わせてファイルを生成します。iOS向けにはFrameworkが生成されます。これをXcodeプロジェクトに組み込むことで、SwiftコードからKotlinコードを呼び出せるようになっています。
しかしながら、このFrameworkはSwiftPackageManagerで管理することができず、障壁となっていました。
そこで、共有ロジックをXCFrameworkとして生成します。XCFrameworkであればbinaryTargetで対象モジュールに対して依存追加を行うことが可能となります。
XCFrameworkの生成方法はKMP公式サイトで紹介されているので、同様のやり方で生成します。
https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks

まず、XCFrameworkを作成するために``shared``ディレクトリのbuild.gradle.ktsを編集します。

```Kotlin:build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

plugins {
    alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
    val xcFramework = XCFramework()
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            xcFramework.add(this)
        }
    }
}

```
build.gradle.ktsでXCFrameworkを宣言したことで、gradleに以下3つのタスクが追加されます。
- assembleReleaseXCFramework
    - ``/shared/build/XCFrameworks/release``にRelease用のXCFrameworkを生成
- assembleDebugXCFramework
    - ``/shared/build/XCFrameworks/debug``にdSYMを含んだXCFrameworkを生成
- assembleXCFramework
    - Debug用、Release用どちらも生成

上記いずれかのタスクを実行することでXCFrameworkが生成されます。
```shell
$ ./gradlew assembleDebugXCFramework
```
:::note info
以下のようにコードを修正することでXCFramework名を変更することができます。
```Kotlin
kotlin {
    val xcFramework = XCFramework("Hoge") // ココ
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "Hoge" // ココ
            xcFramework.add(this)
        }
    }
}
```
この変更を加えるとgradleのタスク名も変更されるので注意してください。
- assembleHogeReleaseXCFramework
- assembleHogeDebugXCFramework
- assembleHogeXCFramework

:::

### Package.swiftでXCFrameworkを取り込む
作成したXCFrameworkをSwiftPackageManagerでiOSプロジェクトに取り込んでいきます。
XCFrameworkを依存追加する場合は、Package.swiftのtargetsにbinaryTargetを追加します。
先ほど生成したXCFrameworkをpathに指定しましょう。
また、共有ロジックを必要とするモジュールのdependenciesにbinaryTargetを追加しましょう。
```swift:Package.swift
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Modules",
    products: [
        .library(
            name: "Modules",
            targets: ["ModuleA"]),
    ],
    targets: [
        .binaryTarget(name: "shared", path: "../../../shared/build/XCFrameworks/debug/shared.xcframework"),
        .target(
            name: "ModuleA", dependencies: ["shared"]),
        .testTarget(
            name: "ModuleATests",
            dependencies: ["ModuleA"]),
    ]
)

```
SwiftPackage内で共有ロジックを利用することができるようになりました。

## Build Phaseからスクリプトを削除する
iOSプロジェクトのBuild Phaseを見ると、もともとFramework形式で共有する際に必要だったスクリプトが設定されています。こちらは不要となったため削除しましょう。
![スクリーンショット 2024-03-27 23.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981389/34afce7f-9f61-135f-428f-e9157fdce897.png)
以上の操作でマルチモジュール構成にできたかと思います。
必要なモジュールを追加していってください！

## 終わりに
KMPプロジェクトであっても、共有ロジックをXCFramework形式で生成することで、マルチモジュール構成のiOSプロジェクトにすることができました。
少しでも参考になれば幸いです。

## 参考文献
- https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks
- https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages
- https://fortee.jp/iosdc-japan-2021/proposal/1338c9cc-363a-44a6-bbac-452b67b472fb