こんな構成を考えたい
iOSでUIとNavigationのみSwift、それ以外をKotlinに寄せるような構成。
この中で、SwiftPMとKMPの統合部分を書く。
KMPのiOS統合方法いろいろ
この辺に詳しくまとまってる。
ローカル, リモートでの統合、XCFramework or frameworkの吐き出しや、cocoapods or script or smp による統合等 やりかたはさまざま。
SPM使いつつ、ロジックを共通化するプロジェクト構成
↑では リモートに一回投げて って手順だが、ローカルで完結させてみる。
iOS側はXcodeから、KMP,AndoridはAndroidStudioからパッケージやモジュールを作る。(パッケージ-モジュール作成方法はそれぞれの開発で自明な気がするので省略)
KMP側で umbrellaモジュールからXCFrameworkを生成
iOSで使用するフレームワークを生成する。
// kmp-ios-umbrella
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
kotlin {
val xcframeworkName = "iOSUmbrella"
val xcf = XCFramework(xcframeworkName)
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = xcframeworkName
// todo replace with your app's bundle ID
binaryOption("bundleId", "org.example.${xcframeworkName}")
xcf.add(this)
isStatic = true
}
}
//...
}
ポイントは、KMP側からiOSに公開するモジュールを1つに絞ること。(Umbrellaモジュール)
Androidのマルチモジュールではあまり気にしない部分だが、iOSはFrameworkとして同じシンボルを複数取り込んでしまうと重複シンボルとしてビルドができない。
この辺に詳しく書いてある(Umbrellaって語彙もここから持ってきた)
昔これ知らなくて後からまとめる形になって大変だった。
ちなみに、KMPWizardのデフォルト設定ではiOSに吐き出すのは通常のframeworkだったりするが、今回のようにXCFramework化した方が楽なはず。
XcodeでRun Scriptを設定
KMP側のXCFrameworkの作成タスクを毎回のビルドで生成されるようにする。
KMPWizardから作ると、Build Phases -> Compile Kotlin Framework というRun Script↓が作られているのでこれを改変する
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
上記のタスクを ./gradlew :shared:assembleiOSUmbrellaXCFramework に書き換え
ちょっと変えているが以下(最終的にタスクが実行されれば問題ない)
# Xcode からの「IDE向けビルドはスキップ」フラグへの対応
if [ "${OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED:-}" = "YES" ]; then
echo "Skipping Gradle build (OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED=YES)"
exit 0
fi
cd "$SRCROOT/.."
pwd
GRADLE_TASK=":shared:assembleiOSUmbrellaXCFramework"
echo "Running Gradle task: $GRADLE_TASK"
./gradlew "$GRADLE_TASK"
umbrellaパッケージからnativeTargetとして取り込み
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "umbrella",
platforms: [.iOS(.v18)],
products: [
.library(
name: "umbrella",
targets: ["iOSUmbrella"]),
],
targets: [
.binaryTarget(
name: "iOSUmbrella",
path: "../../../../shared/build/XCFrameworks/debug/iOSUmbrella.xcframework"
)
]
)
pathの部分は xcframeworkが吐き出されるパスを指定。iOS側に公開したいモジュールになるはず。
ちなみに
※ 未検証
上記だとpath固定しているが、実運用まで考えるとDebug - Releaseで分岐させたいとなるはず。これはおそらくRunScriptでうまく持ってくる必要がある。
cd "$SRCROOT/../" # KMP側のルート
./gradlew :shared:assembleiOSUmbrellaXCFramework
if [ "$CONFIGURATION" = "Release" ]; then
BUILD_SUBDIR="release"
else
BUILD_SUBDIR="debug"
fi
KMP_XCFRAMEWORK="$PWD/${umbrella}/build/xcode-frameworks/$BUILD_SUBDIR/iOSUmbrella.xcframework"
cd "$SRCROOT/../umbrella" # ← KMP側のUmbrellaモジュール
rm -rf iOSUmbrella.xcframework
cp -R "$KMP_XCFRAMEWORK" ./iOSUmbrella.xcframework
とりあえずビルドしてフレームワーク生成して、それをiOS側に持ってきている。
↑でフレームワークがiOS側にくるのでUmbrellaパッケージから読み込み
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "umbrella",
platforms: [.iOS(.v18)],
products: [
.library(
name: "umbrella",
targets: ["iOSUmbrella"]),
],
targets: [
.binaryTarget(
name: "iOSUmbrella",
path: "iOSUmbrella.xcframework"
)
]
)
あとは参照するだけ
ui-a から umbrellaに依存すればimportができる
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "ui-a",
platforms: [.iOS(.v18)],
products: [
.library(
name: "ui-a",
targets: ["ui-a"]),
],
dependencies: [
.package(path: "../umbrella")
],
targets: [
.target(
name: "ui-a",
dependencies: [
.product(name: "umbrella", package: "umbrella")
]
),
.testTarget(
name: "ui-aTests",
dependencies: ["ui-a"]
),
]
)
// ui-a
import SwiftUI
import iOSUmbrella // これ
public struct ScreenA: View {
public init() {}
public var body: some View {
Text("\(Greeting().greet())") // KMP側のclass
}
}
終わり
試してないけど、KMP側のUmbrellaでiOSからもDIのBinds等ができそうなので、おおよその設計はなんとかなるのでわないか。
KMP契機にiOSのNativeちゃんとやりたくなってきたこの頃。
