Kotlin Multiplatform で作ったライブラリを Swift Package Manager 向けに配布する

# 経緯

[Kotlin Multiplatform](https://www.jetbrains.com/ja-jp/kotlin-multiplatform/) で iOS / Android 両方から使える小さなライブラリを作成しました。[Google Analytics for Firebase](https://firebase.google.com/docs/analytics) でイベントを送るにあたり、iOS / Android でイベントが違うことを防ぐために、Kotlin というワンソースでイベントをクラスとして定義して、iOS / Android アプリから使うことを意図しています。
Google Analytics for Firebase の基礎知識やこのライブラリで解決したい課題については、こちらの [DroidKaigi 2024](https://2024.droidkaigi.jp/) 登壇資料をご参照ください。

https://www.docswell.com/s/tfandkusu/KG11N8-DroidKaigi2024

今回紹介するライブラリや GitHub Actions のワークフローは、こちらのリポジトリです。

https://github.com/tfandkusu/ga913-kmp

# Gradle プロジェクトおよびライブラリを作成

[Android Studio](https://developer.android.com/studio) Koala に [Kotlin Multiplatform プラグイン](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform)をインストールしました。Kotlin Multiplatform プロジェクトは New Project → Kotlin Multiplatform Library から作ります。

![スクリーンショット 2024-09-29 12.32.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/042ad3f7-8925-546c-e0d8-c5777c97d9b3.png)

ライブラリの名前を Shared Module Name に設定します。
iOS framework distribution は XCFramework にします。

![スクリーンショット 2024-09-29 13.54.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/e905bc13-dade-3725-9db2-6f035573920c.png)

そうするとモジュールの `build.gradle` には XCFramework を作るための設定が記載されます。

```build.gradle.kts
kotlin {
    // 略 
    val xcf = XCFramework()
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "ga913kmp"
            xcf.add(this)
            isStatic = true
        }
    }    
    // 略
}
```

ライブラリのソースコードは `ga913kmp/src/commonMain/kotlin` に作ります。

![スクリーンショット 2024-09-29 12.46.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/2f661325-f628-ba03-9a65-42cdb9863550.png)

（`jvmMain` `jvmTest` はローカル PC で動作するバリデーションとその単体テストで、記事の主題とは無関係です）

# Swift Package Manager の使い方

次は Xcode で GitHub から Swift Package Manager 向けに配布されているライブラリを使用する方法を紹介します。Xcode は 16.0 を使っています。

ライブラリを使いたいプロジェクトを開いた状態で上部メニューの File → Add Package Dependency を選択します。
右上のテキスト検索欄にライブラリの GitHub ページの URL - 例えば https://github.com/tfandkusu/ga913-kmp を入力するとライブラリの情報が表示されます。ライブラリバージョンも指定できます。次に Add Package ボタンを押します。

![スクリーンショット 2024-09-29 20.56.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/c1346011-6b50-b823-b9b6-dc8e4bd57da6.png)

最後に Target を選んで Add Package ボタンを押すと、ライブラリを選ばれた Target で使えるようになります。

![スクリーンショット 2024-09-30 1.37.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/28884704-f4c4-a41e-8d0e-412eb673de58.png)

# Swift Package Manager から使えるようにライブラリを配布する

前述の方法で使用できるように、GitHub Actions を使いライブラリを自動配布します。
配布のためのワークフローはこちらになります。

https://github.com/tfandkusu/ga913-kmp/blob/main/.github/workflows/release.yml

[VERSION ファイル](https://github.com/tfandkusu/ga913-kmp/blob/main/VERSION)に `0.1.2` 等のようにバージョンを書き込み、`release` ブランチに PUSH すると、ソースコードがビルドされ、ライブラリが配布されます。

ワークフローの内容を解説します。

まず Gradle タスクで XCFramework を作成します。

```sh
./gradlew assembleXCFramework
```

今回のモジュール名は `ga913kmp` なので `ga913kmp/build/XCFrameworks/release/` ディレクトリに作成されます。シミュレータ用と実機用のバイナリが生成されたことを確認できます。

```
cd ga913kmp/build/XCFrameworks/release/ga913kmp.xcframework/
find . -type f
```

```
./ios-arm64_x86_64-simulator/ga913kmp.framework/ga913kmp
./ios-arm64_x86_64-simulator/ga913kmp.framework/Headers/ga913kmp.h
./ios-arm64_x86_64-simulator/ga913kmp.framework/Modules/module.modulemap
./ios-arm64_x86_64-simulator/ga913kmp.framework/Info.plist
./ios-arm64/ga913kmp.framework/ga913kmp
./ios-arm64/ga913kmp.framework/Headers/ga913kmp.h
./ios-arm64/ga913kmp.framework/Modules/module.modulemap
./ios-arm64/ga913kmp.framework/Info.plist
./Info.plist
```

次に生成された XCFramework を ZIP で固めます。

```sh
zip -r ga913kmp.xcframework.zip ga913kmp.xcframework
```

Swift Package Manager は `Package.swift` を読みに行くので `Package.swift` ファイルを作ります。最終的にはこちらのファイルが作られます。

```swift
// swift-tools-version:5.5
import PackageDescription

let package = Package(
    name: "ga913kmp",
    products: [
        .library(
            name: "ga913kmp",
            targets: ["ga913kmp"]
        ),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "ga913kmp",
            url: "https://github.com/tfandkusu/ga913-kmp/releases/download/0.1.0/ga913kmp.xcframework.zip",
            checksum: "54b04d1cad6dce83e100a387e8d54962cdbae7d34e4edb5b3790be84492b81c9"
        ),
    ]
)
```

`url` フィールドに ZIP で固めた XCFramework のダウンロードパスがあり、そのチェックサムが `checksum` フィールドに設定されます。`url` と `checksum` はバージョンごとに異なるので、新バージョンの配布ごとに作成する必要があります。

`url` は [GitHub Release](https://docs.github.com/ja/repositories/releasing-projects-on-github/about-releases) の Asset の URL です。

`checksum` はこちらのコマンドで作ります。

```sh
swift package compute-checksum ga913kmp/build/XCFrameworks/release/ga913kmp.xcframework.zip
```

`Package.swift` ファイルは [ejs テンプレート](https://ejs.co/)から作るようにしました。

```Package.swift.ejs
// swift-tools-version:5.5
import PackageDescription

let package = Package(
    name: "ga913kmp",
    products: [
        .library(
            name: "ga913kmp",
            targets: ["ga913kmp"]
        ),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "ga913kmp",
            url: "https://github.com/tfandkusu/ga913-kmp/releases/download/<%= version %>/ga913kmp.xcframework.zip",
            checksum: "<%= checksum %>"
        ),
    ]
)
```

```sh
VERSION=$(cat VERSION)
CHECKSUM=$(swift package compute-checksum ga913kmp/build/XCFrameworks/release/ga913kmp.xcframework.zip)
npx ejs Package.swift.ejs -i '{"version": "'$VERSION'", "checksum": "'$CHECKSUM'"}' -o Package.swift
```

`Package.swift` ファイルを追加するコミットを作ります。
`[skip ci]` をコミットメッセージに含めると、それについては GitHub Actions が動きません。

```
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add Package.swift
git commit -m "[skip ci]Package.swift を更新"
git push origin HEAD
```

`VERSION` ファイルの内容をタグ名として PUSH し、リリースを作ります。

```sh
echo version=`cat VERSION` >> $GITHUB_ENV
```

```sh
# ${{ env.version }} に VERSION ファイルの内容が設定されている
git tag -a ${{ env.version }} -m "${{ env.version }}"
git push origin ${{ env.version }}
```

```sh
gh release create ${{ env.version }} -t ${{ env.version }} ga913kmp/build/XCFrameworks/release/ga913kmp.xcframework.zip
```

これで完了です。GitHub にはリリースが作られ、Swift Package Manager から使うことができます。

![スクリーンショット 2024-09-30 2.39.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/16506d57-0ee3-0b62-f247-fb4df074b561.png)

# 参考資料

https://www.swift.org/documentation/package-manager/

https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages

https://docs.github.com/ja/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/skipping-workflow-runs