1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【KMP】Koin を使用して Kotlin Multiplatform で Swift パッケージを使用する方法

Last updated at Posted at 2025-03-17

はじめに

本投稿は「How to Use Swift Packages in Kotlin Multiplatform using Koin」からインスパイアされ、そして同投稿を補完するものです。

KMP 公式ドキュメントには以下のように記載があり、Kotlin Multiplatform は 現時点(2025/03/17)で Swift Package Manager には対応していないことが窺えます。

To handle iOS dependencies in Kotlin Multiplatform projects, you can manage them with the cinterop tool or use the CocoaPods dependency manager (pure Swift pods are not supported).

iOS エンジニアなら新しいパッケージを追加するのに CocoaPods パッケージはできれば使いたくないでしょう。また、小生は Fiebase iOS SDK を使うため、公式ドキュメント「Add dependencies on a Pod library」を参考に CocoaPods パッケージの導入を試みたが全く上手くいかず諦めました... 途方に暮れていたところに上記記事を見つけ、問題を解決することができたのです。

本記事は KMP の shared コードにおいて Firebase SDK を使って Firebase Storage にアクセスする方法について記載します。すなわち、androidMain では Firebase Android SDK を使い、iosMain では Firebase iOS SDK を使って、Firebase Storage にアクセスします。

尚、本記事の読者は Koin および Swift Package Manager についてある程度理解できる人を対象にしており、 Koin と Swift Package Manager について詳しく解説しませんのでご了承ください。

本記事では、Koin 4.0.0、Kotlin 2.1.0 を使用しています。

How to Use Swift Packages in Kotlin Multiplatform using Koin」の解析

では、まずオリジナルの記事を読んで見ましょう。(無理して読まなく良いです💦)
記事のコードをシーケンス図にしてみました。難しくないですか? iOS 側と shared 側を行ったり来たり、小生はシーケンス図にするまで何をやってるんか理解できませんでした。
KMP_inject_SPM-ページ1.png

でもシーケンス図にしてもやっぱり難しい。(わかったという人はここから読む必要はもうないですw)

Firebase Storage にアクセスするケースで試す

How to Use Swift Packages in Kotlin Multiplatform using Koin」は Firebase Analytics を扱っていますが、本記事は Firebase Storage を扱う場合について解説していきます。
本記事で扱うコードのシーケンス図は以下です。オリジナルに存在していたKoinApplication のコードは小生には冗長に感じたので削除しました。

KMP_inject_SPM-ページ2.png

クラス図は以下です。

KMP_inject_SPM-ページ3.png

何がしたいか

何がしたいかを先におさえておきましょう。そのために最も重要な箇所は👇です。

コードだとこうなります。single ... という見慣れた Koin のコードですね。

IosModule.kt
fun createSwiftLibDependencyModule(factory: SwiftLibDependencyFactoryContract): Module = module {
    single { factory.provideFirebaseStorageDataSource() } bind FirebaseStorageDataSourceContract::class
}

何をやっているのかというと、Kotlin 側から Swift の provideFirebaseStorageDataSource() を呼び出し、 FirebaseStorageDataSource インスタンスを受け取ってそれを最後に single() で KoinDefinition へ変換しています。これで Swift のオブジェクトを Koin でいつでもどこにでも inject できるようになりました!
つまりやりたいことは Kotlin 側で Swift のオブジェクトをターゲットにして single()factory() を実行することなのです。

以下のコードをみてください。inject の準備ができたらいつもの by inject() を呼んでみます。 すると Swift のオブジェクトが dataSource に格納され、同オブジェクトを操作できるようになりました 🎉

NoticeRepository.kt
class NoticeRepository : KoinComponent {
    suspend fun fetch(): String {
        val dataSource: FirebaseStorageDataSourceContract by inject()
        ...

ここまで来たら「Swift の FirebaseStorageDataSource クラスの中で Firebase iOS SDK に依存するコード書けば良い」ということが容易に想像できると思います。そして Firebase iOS SDK は Swift Package Manger でインストールすれば良いということにも🎵

全体のソースコード

シーケンス図に沿ってソースコード記載していきます。

iOSApp.swift
import SwiftUI
import ComposeApp

@main
struct iOSApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

+   init() {
+       CommonModuleKt.doInitIosKoin(
+           onKoinStart: {
+               IosModuleKt.createSwiftLibDependencyModule(
+                   factory: SwiftLibDependencyFactory.shared
+               )
+           }
+       )
+   }
}
CommonModule.kt
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module

val useCaseModule = module {
    ...
}

val repositoryModule = module {
    ...
}

val viewModelModule = module {
    ...
}

// NOTE: iOS 側の actual platformModule は実際には使わないので空実装で良い。
expect val platformModule: Module

// for Android
fun initAndroidKoin(appDeclaration: KoinAppDeclaration) = startKoin {
    appDeclaration()
    modules(
        viewModelModule,
        useCaseModule,
        repositoryModule,
        platformModule,
    )
}

// for iOS
// CommonModuleKt.doInitIosKoin() として Swift から呼び出す。
-fun initIosKoin() {
+fun initIosKoin(onKoinStart: () -> Module) {
     startKoin {
         modules(
             viewModelModule,
             useCaseModule,
             repositoryModule,
+            onKoinStart(),
         )
     }
 }
SwiftLibDependencyFactoryContract.kt
interface SwiftLibDependencyFactoryContract {
    fun provideFirebaseStorageDataSource(): FirebaseStorageDataSourceContract
}
SwiftLibDependencyFactory.swift
class SwiftLibDependencyFactory: SwiftLibDependencyFactoryContract {
    static var shared = SwiftLibDependencyFactory()

    func provideFirebaseStorageDataSource() -> any FirebaseStorageDataSourceContract {
        return FirebaseStorageDataSource()
    }
}
FirebaseStorageDataSourceContract.kt
interface FirebaseStorageDataSourceContract {
    fun fetch(callback: (String?, Throwable?) -> Unit)
}
FirebaseStorageDataSource.swift
import Foundation
import ComposeApp
import FirebaseStorage
import FirebaseCore

class FirebaseStorageDataSource: ComposeApp.FirebaseStorageDataSourceContract {
    func fetch(callback: @escaping (String?, KotlinThrowable?) -> Void) {
        
        let storage = Storage.storage()
        var noticeRef = storage.reference().child("notice.txt")
        
        noticeRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
            if let error = error {
                print("Error downloading notice.txt: \(error)")
                let kotlinError = KotlinThrowable(message: error.localizedDescription)
                callback(nil, kotlinError)
            } else {
                if let textData = data, let text = String(data: textData, encoding: .utf8) {
                    print("notice.txt content: \(text)")
                    callback(text, nil)
                } else {
                    print("Error: Could not convert data to string.")
                }
            }
        }
    }
}
IosModule.kt
fun createSwiftLibDependencyModule(factory: SwiftLibDependencyFactoryContract): Module = module {
    single { factory.provideFirebaseStorageDataSource() } bind FirebaseStorageDataSourceContract::class
}

Android 側 (= androidMain)

AndroidModule.kt
actual val platformModule = module {
    single {
        val storage = Firebase.storage()
        FirebaseStorageDataSource(storage = storage)
    } bind FirebaseStorageDataSourceContract::class
}
FirebaseStorageDataSource.kt
import com.google.firebase.storage.FirebaseStorage

const val ONE_MEGABYTE: Long = 1024 * 1024

class FirebaseStorageDataSource(
    private val storage: FirebaseStorage
) : FirebaseStorageDataSourceContract {

    override fun fetch(callback: (String?, Throwable?) -> Unit) {
        val noticeRef = storage.reference.child("notice.txt")
        noticeRef.getBytes(ONE_MEGABYTE).addOnSuccessListener { bytes ->
            val text = String(bytes)
            println("notice.txtの内容: $text")
            callback(text, null)
        }.addOnFailureListener { exception ->
            // エラー処理
            println("テキストデータのダウンロードに失敗しました: ${exception.message}")
            callback(null, exception)
        }
    }
}

最後に

単体の iOS アプリを作るときみたく Xcode で Swift パッケージをインストールしてそのラッパークラスを Swift で作っておき、それを Koin を使って shared モジュール側へ inject できるようにしたというのが本記事のまとめです。

この Tips を使うと Swift コードを shared モジュールへ inject できるようになります。なので Swift パッケージを shared モジュール側で使うというのは実はその一ケースでしかないということに気づきます。これで我々は shared モジュールからコールバックを使わずともその場で Swift コードが呼び出せるようになりました!

おまけ

FirebaseStorageDataSourceContractfetch メソッドが suspend ではなくコールバックになっていることに気づいたでしょうか。これは、suspend メソッドは Swift でオーバーライドできないので、suspend の代替としてコールバックにしたからです。しかしコールバックを上位層に影響させたくはなく、かつ Repository 層は suspend を使いたいというのが人情なので以下のように suspendCoroutine を使ってコールバックを待ち受けます。

NoticeRepository.kt
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class NoticeRepository : KoinComponent {
    suspend fun fetch(): String {
        val dataSource: FirebaseStorageDataSourceContract by inject()
        return suspendCoroutine { continuation ->
            dataSource.fetch { notice, error ->
                // TODO: error に対処する
                notice?.let {
                    continuation.resume(it)
                }
            }
        }
    }
}
1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?