Skip to content

iOSとAndroidでより多くのロジックを共有する

このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同じように進めることができます。どちらのIDEも同じコア機能とKotlin Multiplatformサポートを共有しています。


これは、「共有ロジックとネイティブUIを備えたKotlin Multiplatformアプリを作成する」チュートリアルの第4部です。先に進む前に、前のステップを完了していることを確認してください。

First step

Kotlin Multiplatformアプリを作成する
This tutorial uses IntelliJ IDEA, but you can also follow it in Android Studio – both IDEs share the same core functionality and Kotlin Multiplatform support. This is the first part of the Create a Kotlin Multiplatform app with shared logic and native UI tutorial. Create your Kotlin Multiplatform app Update the user interface Add dependencies Share more logic Wrap up your project

Second step
ユーザーインターフェースを更新する
This tutorial uses IntelliJ IDEA, but you can also follow it in Android Studio – both IDEs share the same core functionality and Kotlin Multiplatform support. This is the second part of the Create a Kotlin Multiplatform app with shared logic and native UI tutorial. Before proceeding, make sure you've completed previous steps. Create your Kotlin Multiplatform app Update the user interface Add dependencies Share more logic Wrap up your project

Third step
依存関係を追加する
This tutorial uses IntelliJ IDEA, but you can also follow it in Android Studio – both IDEs share the same core functionality and Kotlin Multiplatform support. This is the third part of the Kotlin Multiplatform app with shared logic and native UI tutorial. Before proceeding, make sure you've completed previous steps. Create your Kotlin Multiplatform app Update the user interface Add dependencies Share more logic Wrap up your project

Fourth step より多くのロジックを共有する
Fifth step プロジェクトを締めくくる

外部依存関係を使用して共通ロジックを実装したので、より複雑なロジックを追加し始めることができます。ネットワークリクエストとデータシリアライズは、Kotlin Multiplatformを使用してコードを共有する最も一般的なユースケースです。このオンボーディングジャーニーを完了した後に将来のプロジェクトでそれらを使用できるように、最初のアプリケーションでそれらを実装する方法を学びましょう。

更新されたアプリは、SpaceX APIからインターネット経由でデータを取得し、SpaceXロケットの最後の成功した打ち上げ日を表示します。

プロジェクトの最終状態は、異なるコルーチンソリューションを持つGitHubリポジトリの2つのブランチで確認できます。

  • mainブランチにはKMP-NativeCoroutinesの実装が含まれています。
  • main-skieブランチにはSKIEの実装が含まれています。

依存関係を追加する

プロジェクトに以下のマルチプラットフォームライブラリを追加する必要があります。

  • kotlinx.coroutines:同時操作を可能にする非同期コードにコルーチンを使用するため。
  • kotlinx.serialization:JSONレスポンスを、ネットワーク操作の処理に使用されるエンティティクラスのオブジェクトにデシリアライズするため。
  • Ktor:インターネット経由でデータを取得するためのHTTPクライアントを作成するためのフレームワーク。

kotlinx.coroutines

kotlinx.coroutinesをプロジェクトに追加するには、共通ソースセットで依存関係を指定します。これを行うには、shared/build.gradle.ktsファイルに次の行を追加します。

kotlin
kotlin {
    // ... 
    sourceSets {
        commonMain.dependencies {
           // ...
           implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
        }
    }
}

Multiplatform Gradleプラグインは、kotlinx.coroutinesのプラットフォーム固有(iOSおよびAndroid)の部分に自動的に依存関係を追加します。

kotlinx.serialization

kotlinx.serializationライブラリを使用するには、対応するGradleプラグインを設定します。 これを行うには、shared/build.gradle.ktsファイルの冒頭にある既存のplugins {}ブロックに次の行を追加します。

kotlin
plugins {
    // ...
    kotlin("plugin.serialization") version "2.2.21"
}

Ktor

共有モジュールの共通ソースセットにコア依存関係(ktor-client-core)を追加する必要があります。 さらに、サポートする依存関係も追加する必要があります。

  • 特定の形式でコンテンツをシリアライズおよびデシリアライズできるContentNegotiation機能(ktor-client-content-negotiation)を追加します。
  • KtorにJSON形式とkotlinx.serializationをシリアライズライブラリとして使用するように指示するために、ktor-serialization-kotlinx-json依存関係を追加します。KtorはJSONデータを期待し、応答を受信したときにそれをデータクラスにデシリアライズします。
  • プラットフォームソースセット(ktor-client-androidktor-client-darwin)の対応するアーティファクトに依存関係を追加することで、プラットフォームエンジンを提供します。
kotlin
kotlin {
    // ...
    val ktorVersion = "3.3.1"

    sourceSets {
        commonMain.dependencies {
            // ...

            implementation("io.ktor:ktor-client-core:$ktorVersion")
            implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        }
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-android:$ktorVersion")
        }
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:$ktorVersion")
        }
    }
}

Sync Gradle Changesボタンをクリックして、Gradleファイルを同期します。

APIリクエストを作成する

データを取得するためにSpaceX APIを使用し、v4/launchesエンドポイントからすべての打ち上げのリストを取得するための単一のメソッドを使用します。

データモデルを追加する

shared/src/commonMain/.../greetingkmpディレクトリに新しいRocketLaunch.ktファイルを作成し、SpaceX APIからデータを格納するデータクラスを追加します。

kotlin
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class RocketLaunch (
    @SerialName("flight_number")
    val flightNumber: Int,
    @SerialName("name")
    val missionName: String,
    @SerialName("date_utc")
    val launchDateUTC: String,
    @SerialName("success")
    val launchSuccess: Boolean?,
)
  • RocketLaunchクラスには@Serializableアノテーションが付けられているため、kotlinx.serializationプラグインは自動的にデフォルトのシリアライザーを生成できます。
  • @SerialNameアノテーションを使用すると、フィールド名を再定義できるため、データクラスでプロパティをより読みやすい名前で宣言できます。

HTTPクライアントを接続する

  1. shared/src/commonMain/.../greetingkmpディレクトリに新しいRocketComponentクラスを作成します。

  2. HTTP GETリクエストを通じてロケット打ち上げ情報を取得するためのhttpClientプロパティを追加します。

    kotlin
    import io.ktor.client.HttpClient
    import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
    import io.ktor.serialization.kotlinx.json.json
    import kotlinx.serialization.json.Json
    
    class RocketComponent {
        private val httpClient = HttpClient {
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
        }
    }
    • ContentNegotiation KtorプラグインとJSONシリアライザーは、GETリクエストの結果をデシリアライズします。
    • ここでのJSONシリアライザーは、prettyPrintプロパティによりJSONをより読みやすい形式で出力するように設定されています。isLenientにより不正な形式のJSONを読み取る際に柔軟性が高まり、ignoreUnknownKeysによりロケット打ち上げモデルで宣言されていないキーを無視します。
  3. RocketComponentgetDateOfLastSuccessfulLaunch()サスペンド関数を追加します。

    kotlin
    class RocketComponent {
        // ...
        
        private suspend fun getDateOfLastSuccessfulLaunch(): String {
        
        }
    }
  4. httpClient.get()関数を呼び出して、ロケット打ち上げ情報を取得します。

    kotlin
    import io.ktor.client.request.get
    import io.ktor.client.call.body
    
    class RocketComponent {
        // ...
        
        private suspend fun getDateOfLastSuccessfulLaunch(): String {
            val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body()
        }
    }
    • httpClient.get()もサスペンド関数です。これは、スレッドをブロックせずにネットワーク経由で非同期にデータを取得する必要があるためです。
    • サスペンド関数は、コルーチンまたは他のサスペンド関数からのみ呼び出すことができます。これがgetDateOfLastSuccessfulLaunch()suspendキーワードでマークされた理由です。ネットワークリクエストはHTTPクライアントのスレッドプールで実行されます。
  5. 関数を再度更新して、リスト内の最後の成功した打ち上げを見つけます。

    kotlin
    class RocketComponent {
        // ...
        
        private suspend fun getDateOfLastSuccessfulLaunch(): String {
            val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body()
            val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
        }
    }

    ロケット打ち上げのリストは、古いものから新しいものへと日付順にソートされています。

  6. 打ち上げ日をUTCからローカル日時に変換し、出力をフォーマットします。

    kotlin
    import kotlinx.datetime.TimeZone
    import kotlinx.datetime.toLocalDateTime
    import kotlin.time.ExperimentalTime
    import kotlin.time.Instant
    
    class RocketComponent {
        // ...
        
        @OptIn(ExperimentalTime::class)
        private suspend fun getDateOfLastSuccessfulLaunch(): String {
            val rockets: List<RocketLaunch> =
                httpClient.get("https://api.spacexdata.com/v4/launches").body()
            val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
            val date = Instant.parse(lastSuccessLaunch.launchDateUTC)
                .toLocalDateTime(TimeZone.currentSystemDefault())
        
            return "${date.month} ${date.day}, ${date.year}"
        }
    }

    日付は「MMMM DD, YYYY」形式になります(例:OCTOBER 5, 2022)。

  7. getDateOfLastSuccessfulLaunch()関数を使用してメッセージを作成する、もう1つのサスペンド関数launchPhrase()を追加します。

    kotlin
    class RocketComponent {
        // ...
    
        suspend fun launchPhrase(): String =
            try {
                "The last successful launch was on ${getDateOfLastSuccessfulLaunch()} 🚀"
            } catch (e: Exception) {
                println("Exception during getting the date of the last successful launch $e")
                "Error occurred"
            }
    }

Flowを作成する

サスペンド関数の代わりにFlowを使用できます。これらは、サスペンド関数が返す単一の値ではなく、値のシーケンスを発行します。

  1. shared/src/commonMain/kotlinディレクトリにあるGreeting.ktファイルを開きます。

  2. GreetingクラスにrocketComponentプロパティを追加します。このプロパティには、最後の成功した打ち上げ日を含むメッセージが格納されます。

    kotlin
    private val rocketComponent = RocketComponent()
  3. greet()関数がFlowを返すように変更します。

    kotlin
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.flow.flow
    import kotlin.time.Duration.Companion.seconds
    
    class Greeting {
        // ...
        fun greet(): Flow<String> = flow {
            emit(if (Random.nextBoolean()) "Hi!" else "Hello!")
            delay(1.seconds)
            emit("Guess what this is! > ${platform.name.reversed()}")
            delay(1.seconds)
            emit(daysPhrase())
            emit(rocketComponent.launchPhrase())
        }
    }
    • Flowは、すべてのステートメントをラップするflow()ビルダー関数でここに作成されます。
    • Flowは、各発行間に1秒の遅延を伴って文字列を発行します。最後の要素は、ネットワーク応答が返された後にのみ発行されるため、正確な遅延はネットワークによって異なります。

インターネットアクセス権限を追加する

インターネットにアクセスするには、Androidアプリケーションに適切な権限が必要です。すべてのネットワークリクエストは共有モジュールから行われるため、そのマニフェストにインターネットアクセス権限を追加するのが理にかなっています。

composeApp/src/androidMain/AndroidManifest.xmlファイルをアクセス権限で更新します。

xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

greet()関数の戻り値の型をFlowに変更することで、共有モジュールのAPIはすでに更新されています。 次に、greet()関数呼び出しの結果を適切に処理できるように、プロジェクトのネイティブ部分を更新する必要があります。

ネイティブAndroid UIを更新する

共有モジュールとAndroidアプリケーションの両方がKotlinで記述されているため、Androidから共有コードを使用するのは簡単です。

ビューモデルを導入する

アプリケーションがより複雑になるにつれて、UIを実装するApp()関数を呼び出すAndroidアクティビティであるMainActivityにビューモデルを導入する時が来ました。 ビューモデルはアクティビティからのデータを管理し、アクティビティがライフサイクル変更を受けても消滅しません。

  1. composeApp/src/androidMain/.../greetingkmpディレクトリに、新しいMainViewModel Kotlinクラスを作成します。

    kotlin
    import androidx.lifecycle.ViewModel
    
    class MainViewModel : ViewModel() {
        // ...
    }

    このクラスはAndroidのViewModelクラスを拡張しており、ライフサイクルと設定変更に関して正しい動作を保証します。

  2. StateFlow型のgreetingList値と、そのバッキングプロパティを作成します。

    kotlin
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.StateFlow
    
    class MainViewModel : ViewModel() {
        private val _greetingList = MutableStateFlow<List<String>>(listOf())
        val greetingList: StateFlow<List<String>> get() = _greetingList
    }
    • ここでのStateFlowFlowインターフェースを拡張していますが、単一の値または状態を持ちます。
    • プライベートなバッキングプロパティ_greetingListは、このクラスのクライアントのみが読み取り専用のgreetingListプロパティにアクセスできることを保証します。
  3. View Modelのinit関数で、Greeting().greet()フローからすべての文字列を収集します。

    kotlin
    import androidx.lifecycle.viewModelScope
    import kotlinx.coroutines.launch
    
    class MainViewModel : ViewModel() {
       private val _greetingList = MutableStateFlow<List<String>>(listOf())
       val greetingList: StateFlow<List<String>> get() = _greetingList
       
       init {
           viewModelScope.launch {
               Greeting().greet().collect { phrase ->
                    //...
               }
           }
       }
    }

    collect()関数はサスペンドされるため、ビューモデルのスコープ内でlaunchコルーチンが使用されます。 これは、launchコルーチンがビューモデルのライフサイクルの正しいフェーズ中のみ実行されることを意味します。

  4. collectの後続ラムダ内で、収集されたphraselist内のフレーズのリストに追加するように_greetingListの値を更新します。

    kotlin
    import kotlinx.coroutines.flow.update
    
    class MainViewModel : ViewModel() {
        //...
    
        init {
            viewModelScope.launch {
                Greeting().greet().collect { phrase ->
                    _greetingList.update { list -> list + phrase }
                }
            }
        }
    }

    update()関数は値を自動的に更新します。

ビューモデルのFlowを使用する

  1. composeApp/src/androidMain/kotlinにあるApp.ktファイルを開き、以前の実装を置き換えるように更新します。

    kotlin
    import androidx.lifecycle.compose.collectAsStateWithLifecycle
    import androidx.compose.runtime.getValue
    import androidx.lifecycle.viewmodel.compose.viewModel
    
    @Composable
    @Preview
    fun App(mainViewModel: MainViewModel = viewModel()) {
        MaterialTheme {
            val greetings by mainViewModel.greetingList.collectAsStateWithLifecycle()
    
            Column(
                modifier = Modifier
                    .safeContentPadding()
                    .fillMaxSize(),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                greetings.forEach { greeting ->
                    Text(greeting)
                    HorizontalDivider()
                }
            }
        }
    }
    • greetingListに対するcollectAsStateWithLifecycle()関数呼び出しは、ViewModelのFlowから値を収集し、ライフサイクルを意識した方法でそれをコンポーザブルステートとして表現します。
    • 新しいFlowが作成されると、コンポーズの状態が変更され、区切り線で区切られたグリーティングフレーズが垂直に配置されたスクロール可能なColumnが表示されます。
  2. 結果を確認するには、composeApp構成を再実行します。

Final results

ネイティブiOS UIを更新する

プロジェクトのiOS部分では、ビジネスロジックをすべて含む共有モジュールにUIを接続するために、Model–view–viewmodelパターンを再び利用します。

モジュールはContentView.swiftファイルにimport Shared宣言で既にインポートされています。

ViewModelを導入する

iosApp/ContentView.swiftで、ContentViewViewModelクラスを作成し、それのためのデータを準備および管理します。 並行処理をサポートするために、startObserving()関数をtask()呼び出し内で呼び出します。

swift
import SwiftUI
import Shared

struct ContentView: View {
    @ObservedObject private(set) var viewModel: ViewModel

    var body: some View {
        ListView(phrases: viewModel.greetings)
            .task { await self.viewModel.startObserving() }
    }
}

extension ContentView {
    @MainActor
    class ViewModel: ObservableObject {
        @Published var greetings: Array<String> = []
        
        func startObserving() {
            // ...
        }
    }
}

struct ListView: View {
    let phrases: Array<String>

    var body: some View {
        List(phrases, id: \.self) {
            Text($0)
        }
    }
}
  • ViewModelContentViewの拡張として宣言されており、密接に関連しています。
  • ViewModelには、Stringフレーズの配列であるgreetingsプロパティがあります。 SwiftUIはViewModel(ContentView.ViewModel)をビュー(ContentView)に接続します。
  • ContentView.ViewModelObservableObjectとして宣言されています。
  • @Publishedラッパーはgreetingsプロパティに使用されます。
  • @ObservedObjectプロパティラッパーはViewModelを購読するために使用されます。

このViewModelは、このプロパティが変更されるたびにシグナルを発行します。 次に、Flowを消費するためにstartObserving()関数を実装する必要があります。

iOSからFlowを消費するためのライブラリを選択する

このチュートリアルでは、iOSでFlowを操作するのに役立つSKIEまたはKMP-NativeCoroutinesライブラリを使用できます。 どちらもオープンソースソリューションであり、Kotlin/Nativeコンパイラがまだデフォルトで提供していないFlowによるキャンセルとジェネリクスをサポートしています。

  • SKIEライブラリは、Kotlinコンパイラによって生成されたObjective-C APIを拡張します。SKIEはFlowをSwiftのAsyncSequenceと同等のものに変換します。SKIEは、スレッド制限なしで、自動的な双方向キャンセルを伴うSwiftのasync/awaitを直接サポートします(CombineとRxSwiftにはアダプターが必要です)。SKIEは、さまざまなKotlin型をSwiftの同等型にブリッジすることを含め、KotlinからSwiftフレンドリーなAPIを生成するための他の機能も提供します。また、iOSプロジェクトに追加の依存関係を追加する必要もありません。
  • KMP-NativeCoroutinesライブラリは、必要なラッパーを生成することで、iOSからサスペンド関数とFlowを消費するのに役立ちます。 KMP-NativeCoroutinesは、Swiftのasync/await機能、Combine、RxSwiftをサポートしています。 KMP-NativeCoroutinesを使用するには、iOSプロジェクトにSPMまたはCocoaPodの依存関係を追加する必要があります。

オプション1. KMP-NativeCoroutinesを構成する

ライブラリの最新バージョンを使用することをお勧めします。 プラグインの新しいバージョンが利用可能かどうかは、KMP-NativeCoroutinesリポジトリで確認してください。

  1. プロジェクトのルートbuild.gradle.ktsファイル(shared/build.gradle.ktsファイルではない)のplugins {}ブロックにKSP (Kotlin Symbol Processor)とKMP-NativeCoroutinesプラグインを追加します。

    kotlin
    plugins {
        // ...
        id("com.google.devtools.ksp").version("2.2.10-2.0.2").apply(false)
        id("com.rickclephas.kmp.nativecoroutines").version("1.0.0-ALPHA-45").apply(false)
    }
  2. shared/build.gradle.ktsファイルにKMP-NativeCoroutinesプラグインを追加します。

    kotlin
    plugins {
        // ...
        id("com.google.devtools.ksp")
        id("com.rickclephas.kmp.nativecoroutines")
    }
  3. 同じくshared/build.gradle.ktsファイルで、実験的な@ObjCNameアノテーションをオプトインします。

    kotlin
    kotlin {
        // ...
        sourceSets{
            all {
                languageSettings {
                    optIn("kotlin.experimental.ExperimentalObjCName")
                    optIn("kotlin.time.ExperimentalTime")
                }
            }
            // ...
        }
    }
  4. Sync Gradle Changesボタンをクリックして、Gradleファイルを同期します。

KMP-NativeCoroutinesでFlowをマークする

  1. shared/src/commonMain/kotlinディレクトリのGreeting.ktファイルを開きます。

  2. greet()関数に@NativeCoroutinesアノテーションを追加します。これにより、プラグインがiOSでの正しいFlow処理をサポートするための適切なコードを生成することを保証します。

    kotlin
    import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
    
    class Greeting {
        // ...
       
        @NativeCoroutines
        fun greet(): Flow<String> = flow {
            // ...
        }
    }

XcodeでSPMを使用してライブラリをインポートする

  1. File | Open Project in Xcode に移動します。

  2. Xcodeで、左側のメニューにあるiosAppプロジェクトを右クリックし、Add Package Dependenciesを選択します。

  3. 検索バーにパッケージ名を入力します。

    none
    https://github.com/rickclephas/KMP-NativeCoroutines.git

Importing KMP-NativeCoroutines

  1. Dependency RuleドロップダウンでExact Version項目を選択し、隣接するフィールドに1.0.0-ALPHA-45バージョンを入力します。
  2. Add Packageボタンをクリックします。XcodeはGitHubからパッケージをフェッチし、別のウィンドウを開いてパッケージプロダクトを選択します。
  3. 表示されているように、「KMPNativeCoroutinesAsync」と「KMPNativeCoroutinesCore」をアプリに追加し、Add Packageをクリックします。

Add KMP-NativeCoroutines packages

これにより、async/awaitメカニズムを操作するために必要なKMP-NativeCoroutinesパッケージの一部がインストールされます。

KMP-NativeCoroutinesライブラリを使用してFlowを消費する

  1. iosApp/ContentView.swiftで、KMP-NativeCoroutinesのasyncSequence()関数を使用してGreeting().greet()関数にFlowを消費するようにstartObserving()関数を更新します。

    Swift
    func startObserving() async {
        do {
            let sequence = asyncSequence(for: Greeting().greet())
            for try await phrase in sequence {
                self.greetings.append(phrase)
            }
        } catch {
            print("Failed with error: \(error)")
        }
    }

    ここでのループとawaitメカニズムは、Flowを反復処理し、Flowが値を放出するたびにgreetingsプロパティを更新するために使用されます。

  2. ViewModel@MainActorアノテーションでマークされていることを確認します。このアノテーションは、ViewModel内のすべての非同期操作がKotlin/Nativeの要件に準拠するためにメインスレッドで実行されることを保証します。

    Swift
    // ...
    import KMPNativeCoroutinesAsync
    import KMPNativeCoroutinesCore
    
    // ...
    extension ContentView {
        @MainActor
        class ViewModel: ObservableObject {
            @Published var greetings: Array<String> = []
    
            func startObserving() async {
                do {
                    let sequence = asyncSequence(for: Greeting().greet())
                    for try await phrase in sequence {
                        self.greetings.append(phrase)
                    }
                } catch {
                    print("Failed with error: \(error)")
                }
            }
        }
    }

オプション2. SKIEを構成する

ライブラリを設定するには、shared/build.gradle.ktsにSKIEプラグインを指定し、Sync Gradle Changesボタンをクリックします。

kotlin
plugins {
   id("co.touchlab.skie") version "0.10.6"
}

執筆時点での最新であるSKIEのバージョン0.10.6は、最新のKotlinをサポートしていません。これを使用するには、gradle/libs.versions.tomlファイルでKotlinのバージョンを2.2.10にダウングレードしてください。

SKIEを使用してFlowを消費する

Greeting().greet() Flowを反復処理し、Flowが値を放出するたびにgreetingsプロパティを更新するために、ループとawaitメカニズムを使用します。

ViewModel@MainActorアノテーションでマークされていることを確認します。 このアノテーションは、ViewModel内のすべての非同期操作がKotlin/Nativeの要件に準拠するためにメインスレッドで実行されることを保証します。

Swift
// ...
extension ContentView {
    @MainActor
    class ViewModel: ObservableObject {
        @Published var greetings: [String] = []

        func startObserving() async {
            for await phrase in Greeting().greet() {
                self.greetings.append(phrase)
            }
        }
    }
}

ViewModelを消費し、iOSアプリを実行する

iosApp/iOSApp.swiftで、アプリのエントリポイントを更新します。

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

IntelliJ IDEAからiosApp構成を実行して、アプリのロジックが同期されていることを確認します。

Final results

プロジェクトの最終状態は、異なるコルーチンソリューションを持つGitHubリポジトリの2つのブランチで確認できます。

  • mainブランチにはKMP-NativeCoroutinesの実装が含まれています。
  • main-skieブランチにはSKIEの実装が含まれています。

次のステップ

チュートリアルの最終部では、プロジェクトを締めくくり、次に取るべきステップを確認します。

次のパートに進む

参照

ヘルプを得る