Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose MultiplatformにおけるiOSネイティブ実装のベストプラクティス

Compose MultiplatformにおけるiOSネイティブ実装のベストプラクティス

Ebisu.mobile #9 の発表資料です。
https://hey.connpass.com/event/347637/

資料の中で示している URL です
https://github.com/0x6368656174/kmp-di-example

Kenta Enomoto

March 24, 2025
Tweet

More Decks by Kenta Enomoto

Other Decks in Technology

Transcript

  1. CMP における iOS アプリ開発の課題 • Kotlin の共通実装だけで両 OS の実装が完了しないケースがある ◦

    ネイティブ API を使いたいケース ▪ 例:音を鳴らす ◦ Firebase のような mBaaS の利用 3
  2. おさらい:expect / actual とは 6 expect class AudioPlayer { fun

    playSound(audio: Audio) } commonMain/AudioPlayer.kt actual class AudioPlayer { actual fun playSound(audio: Audio) { val mediaItem = NSURL.URLWithString(URLString = Res.getUri(audio.path)) ??. } } iosMain/AudioPlayer.ios.kt actual class AudioPlayer( private val context: Context, ) { actual fun playSound(audio: Audio) { val soundPool = requireSoundPool() ??. } } androidMain/AudioPlayer.android.kt
  3. チームの意思決定:Swift を積極的に使う • iOS 専用の実装が必要な箇所は Swift でなるべく書くことに決めた • 前提:Android エンジニア2名、

    iOS エンジニア2名の混成チーム ◦ つまり、チームには iOS の専門家がいる • Swift を使って書けるなら、なるべくそうしたい • Swift Package Manager を用いて純正のライブラリを利用できる 8
  4. DI の実装方法と設計 - Kotlin 側に interface を定義、Swift で実装 11 final

    class KrashlyticsCoreImpl: CoreKrashlyticsCore { private var crashlytics: Crashlytics? init() { crashlytics = Crashlytics.crashlytics() } func log(message: String) { … } … } iosApp/KrashlyticsCoreImpl.swift interface KrashlyticsCore { fun log(message: String) ??. } commonMain/Krashlytics.kt
  5. DI の実装方法と設計 - iOS のエントリーポイントで依存性を注入 12 @main struct iOSApp: SwiftUI.App

    { init() { FirebaseApp.configure() InitKoinKt.doInitKoin(config: nil, iosNativeModule: iosNativeModule) sharedUi.App.companion.initialize( krashlyticsCore: inject(), remoteConfigCore:inject() ) } var body: some Scene { … } } var iosNativeModule: Koin_coreModule = MakeNativeModuleKt.makeNativeModule( krashlyticsCore: { _ in return KrashlyticsCoreImpl() }, remoteConfigCore: { _ in return RemoteConfigCoreImpl() } ) iosApp/iOSApp.swift
  6. DI の実装方法と設計 - iOS のエントリーポイントで依存性を注入 13 commonMain/InitKoin.kt fun initKoin( config:

    KoinAppDeclaration? = null, iosNativeModule: Module? = null, ) { val modules = mutableListOf( commonSharedUiModule, commonCoreDomainModule, platformCoreDataModule, platformCoreAudioModule, ) iosNativeModule?.let { modules.add(it) } KoinDispatcher.start( modules = modules, config = config, ) }
  7. DI の実装方法と設計 - iOS のエントリーポイントで依存性を注入 14 commonMain/InitKoin.kt fun initKoin( config:

    KoinAppDeclaration? = null, iosNativeModule: Module? = null, ) { val modules = mutableListOf( commonSharedUiModule, commonCoreDomainModule, platformCoreDataModule, platformCoreAudioModule, ) iosNativeModule?.let { modules.add(it) } KoinDispatcher.start( modules = modules, config = config, ) } @main struct iOSApp: SwiftUI.App { init() { FirebaseApp.configure() InitKoinKt.doInitKoin(config: nil, iosNativeModule: iosNativeModule) sharedUi.App.companion.initialize( krashlyticsCore: KrashlyticsCoreImpl(), remoteConfigCore: RemoteConfigCoreImpl() ) } var body: some Scene { … } } var iosNativeModule: Koin_coreModule = MakeNativeModuleKt.makeNativeModule( krashlyticsCore: { _ in return KrashlyticsCoreImpl() }, remoteConfigCore: { _ in return RemoteConfigCoreImpl() } ) iosApp/iOSApp.swift (再掲)
  8. 補足: DI の実装方法と設計 - commonMain から Swift オブジェクトを参照 • ジェネリクスや

    Swift Type metadataを用いることで、 Kotlin の共 通コード内でも Swift のオブジェクトを参照することが可能 15
  9. 補足: DI の実装方法と設計 - iOS のエントリーポイントで依存性を注入 16 @main struct iOSApp:

    SwiftUI.App { init() { FirebaseApp.configure() InitKoinKt.doInitKoin(config: nil, iosNativeModule: iosNativeModule) sharedUi.App.companion.initialize( krashlyticsCore: inject(), remoteConfigCore: inject() ) } var body: some Scene { // 省略 } } iosApp/iOSApp.swift (再掲)
  10. 補足: DI の実装方法と設計 - commonMain から Swift オブジェクトを参照 17 class

    SwiftKClass<T>: NSObject, KotlinKClass { func isInstance(value: Any?) -> Bool { value is T } var qualifiedName: String? { String(reflecting: T.self) } var simpleName: String? { String(describing: T.self) } } func KClass<T>(for type: T.Type) -> KotlinKClass { SwiftType(type: type, swiftClazz: SwiftKClass<T>()).getClazz() } extension Koin_coreScope { func get<T>() -> T { get(clazz: KClass(for: T.self), qualifier: nil, parameters: nil) as! T } } func inject<T>( qualifier: Koin_coreQualifier? = nil, parameters: (() -> Koin_coreParametersHolder)? = nil ) -> T { KoinGetKt.koinGet(clazz: KClass(for: T.self), qualifier: qualifier, parameters: parameters) as! T } iosApp/KoinHelpers.swift https://github.com/0x6368656174/kmp-di-example This slide includes source code licensed under the Apache License, Version 2.0. © [Pavel Puchkov] You may obtain a copy of the license at http://www.apache.org/licenses/LICENSE-2.0