LoginSignup
2
1

Kotlin Multiplatform の SKIE について Flows 機能の動作を TCA を使って確認しました

Last updated at Posted at 2023-12-30

はじめに

Kotlin Multiplatform における iOS 向け API を Swift から使いやすくするツール SKIE (スカイ)を活用しています。これまでいくつかある機能のうち、Sealed ClassesSuspend Functions を紹介してきました。今回は Flows 機能を iOS のありものの状態管理クラスである TCA の Reducer に乗せて動作確認する形で紹介しようと思います。

Sealed Classes 機能の紹介と導入事例

Suspend Functions 機能の紹介

Kotlin 側の実装を作成

値を保持する MutableStateFlow を作成して、Flow としての取得と値への加算ができる Kotlin の関数を作ります。

Example.kt
private val exampleMutableStateFlow = MutableStateFlow(0)

fun getExampleFlow(): Flow<Int> {
    return exampleMutableStateFlow
}

fun countUpExampleFlowValue() {
    ++exampleMutableStateFlow.value
}

SKIE を使わずに TCA から Flow を使う

TCA の Reducer から上記の Kotlin 関数を呼び出します。TCA の書き方は公式サンプルの 02-Effects-LongLiving.swift を参考にしています。この時点では SKIE は導入していません。よって Flow は Kotlinx_coroutines_coreFlowCollector プロトコルに準拠したクラスを作って監視します。

FlowExpReducer.swift
import ComposableArchitecture
import shared

@Reducer
struct FlowExpReducer {
    struct State: Equatable {
        var count = 0
    }

    enum Action {
        case task
        // Count Up ボタンが押されたときに呼ばれる
        case countUp
        // 表示カウントを更新する
        case updateCount(count: Int)
    }

    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .task:
                return .run { send in
                    class Collector: Kotlinx_coroutines_coreFlowCollector {
                        let sendTask: (Int) async -> Void

                        init(sendTask: @escaping (Int) async -> Void) {
                            self.sendTask = sendTask
                        }

                        func emit(value: Any?) async throws {
                            if let intValue = value as! Int? {
                                await self.sendTask(intValue)
                            }
                        }
                    }
                    let collector = Collector(
                        sendTask: { intValue in
                            await send(.updateCount(count: intValue))
                        }
                    )
                    try await ExampleKt.getExampleFlow().collect(collector: collector)
                }
            case .countUp:
                ExampleKt.countUpExampleFlowValue()
                return .none
            case let .updateCount(count: count):
                state.count = count
                return .none
            }
        }
    }
}

見た目はこのようなものを作りました。「Count Up」ボタンを押すと、Kotlin 側の MutableStateFlow の値が1増えて、その値が表示されます。

スクリーンショット 2023-12-30 17.35.53.png

FlowExpView.swift
import ComposableArchitecture
import SwiftUI

struct FlowExpView: View {
    let store: StoreOf<FlowExpReducer>

    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            VStack {
                Text("Count = \(viewStore.count)").padding(8)
                Button(
                    action: {
                        viewStore.send(.countUp)
                    }
                ) {
                    Text("Count Up")
                }.padding(8)
                Spacer()
            }.navigationBarTitle("Flow 動作確認")
                .task {
                    await viewStore.send(.task).finish()
                }
        }
    }
}

しかしこれはクラッシュします。
理由は

try await ExampleKt.getExampleFlow().collect(collector: collector)

の行が suspend 関数になっているためです。前回の suspend 関数の記事で解説した通り、Kotlin の suspend 関数はメインスレッドからしか呼べず、FlowExpReducer の .run に渡したブロックの中はメインスレッドで無いためです。

SKIE の Suspend Functions 機能のみ有効化する

SKIE を導入し Flows 機能を無効化することで Suspend Functions 機能のみ有効化します。導入方法はこちらにあり、設定方法はこちらにあります。

build.gradle
plugins {
    id("co.touchlab.skie") version "0.6.1"
}

// 略

skie {
    features {
        group {
            FlowInterop.Enabled(false)
        }
    }
}

ビルドが通るように Reducer の .run ブロックを少し変更します。ついでに SKIE の Global Functions 機能も有効になっているので、クラス名を無くしました。さらに collect メソッドが適切にキャンセルされているか確認できるように CancellationError を Catch したら標準出力するようにしました。

FlowExpReducer.swift
return .run { send in
    class Collector: Kotlinx_coroutines_coreFlowCollector {
        let sendTask: (Int) async -> Void

        init(sendTask: @escaping (Int) async -> Void) {
            self.sendTask = sendTask
        }

        // SKIE でメソッド名が変化している
        func __emit(value: Any?) async throws {
            if let intValue = value as! Int? {
                await self.sendTask(intValue)
            }
        }
    }
    let collector = Collector(
        sendTask: { intValue in
            await send(.updateCount(count: intValue))
        }
    )
    do {
        // Global Functions 機能でクラス名の指定不要
        try await getExampleFlow().collect(collector: collector)
    } catch {
        // 処理がキャンセルされると、CancellationError 例外が発生する
        if error is CancellationError {
            // キャンセルされたことが分かるように標準出力する
            print("CancellationError")
        }
    }
}

これは動作します。

Simulator Screen Recording - iPhone 15 Pro - 2023-12-30 at 20.59.36.gif

画面を閉じると CancellationError 例外が発生するため、再度画面を開いても Flow は多重に監視されません。

SKIE の Flows 機能を使う

SKIE の Suspend Functions 機能だけでも動作はしますが Kotlinx_coroutines_coreFlowCollector プロトコルに準拠するクラスを作ったり、Any? 型をキャストしたりと活用が面倒です。ここで SKIE の Flows 機能を有効にするとどのように書けるかを見てみます。

公式の Example に従ってこのように書けます。

FlowExpReducer.swift
return .run { send in
    for await value in getExampleFlow() {
        await send(.updateCount(count: Int(truncating: value)))
    }
}

valueKotlinInt 型だったので Xcode のエラーおよび警告の Fix ボタンからの提案に従い Int(truncating: value) で Int 型に変換しました。もちろん画面を閉じると for ループを抜けることができます。

SKIE によって自動生成された getExampleFlow 関数の実装はこのようになっていました。

public func getExampleFlow() -> shared.SkieSwiftFlow<shared.KotlinInt> {
    return shared.ExampleKt.getExampleFlow()
}

SKIE 公式の解説にあるように SkieSwiftFlow が Swift の AsyncSequence プロトコルに準拠しているのでこのように書けます。

まとめ

モバイルアプリ開発には、他の画面で行われた更新を抜けなく反映する所謂「いいね問題」があります。そのひとつの解決方法は信頼できる唯一の情報源の値が流れてくる Flow を取得、監視することで、リアルタイムに UI に反映する作りにすることです。

しかし状態ホルダーが Swift でドメインレイヤーが Kotlin で書かれているケースの場合、Kotlin Multiplatform だけでは Flow の活用は難解です。そこで SKIE の Flows 機能を使うことで、Kotlin の Flow は Swift の AsyncSequence プロトコルに準拠したクラスに変換されるため、Swift から容易に活用できます。そのことを今回は TCA を使って確認することができました。

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