1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift Package ManagerのiOS構成とKMPを統合するプロジェクト構成を考える

Posted at

こんな構成を考えたい

module.png

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ちゃんとやりたくなってきたこの頃。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?