はじめに
先日、Kotlin Multiplatform における iOS 向け API を Swift から使いやすくするツール SKIE の機能 のうち、 Sealed Classes を紹介しました。導入方法もこちらで解説しています。
今回は Suspend Functions についての紹介と The Composable Architecture (TCA) を用いた動作確認を行います。
ちなみに SKIE の発音は空の「スカイ」です。
公式ドキュメント
公式ドキュメントの Suspend Functions には、このように書かれています。
The Kotlin compiler outputs suspend functions and you can call them from Swift with the async/await syntax.
Kotlin コンパイラは suspend 関数を出力し、Swift から async/await 構文で呼ぶことができる。
However, these functions are not cancellable and by default they need to be called from the main thread, or they can crash the runtime.
しかしながら、これらの関数はキャンセルできない。またメインスレッドから呼ぶ必要があり、そうでないと実行時にクラッシュする。
SKIE exposes suspend functions that cooperate with both Swift and Kotlin's concurrency contexts. That means automatic two-way cancellation, and no thread restrictions.
SKIE は Swift と Kotlin で協調する concurrency contexts を公開している。それは自動での双方向のキャンセルとスレッドの制限がないことを意味している。
SKIE を導入することで、キャンセルとメインスレッド以外からの呼び出しができるようです。この記事ではキャンセルとメインスレッド以外からの呼び出しが実際にできるか TCA を使って動作確認を行います。
動作確認用の Kotlin の suspend 関数を作成
今回の動作確認用に Kotlin 側にシンプルな suspend 関数を作成しました。
suspend fun exampleFunction() {
// 3秒待って終了
delay(3000)
}
動作確認用の TCA の Reducer を作成
今回の動作確認用に、ありものの SwiftUI の状態管理クラスである TCA の Reducer のクラスを作成しました。書き方は TCA の GitHub トップページやサンプルの Search を参考にしています。
struct SuspendReducer: Reducer {
// キャンセル ID を定義
private enum CancelID { case example }
func reduce(into state: inout State, action: Action) -> Effect<Action> {
// 呼ばれた action に対して現在の state を次の state へ更新したり、次に呼ぶ action を返却する。
switch action {
case .callFunction:
state.status = .running
// この処理のキャンセルは CancelID.example を指定することで行えるようにした
return .run { send in
do {
// Kotlin の suspend 関数呼び出し
try await ExampleKt.exampleFunction()
// 処理の完了アクションを返却
await send(.completeFunction)
} catch {
// 処理がキャンセルされると CancellationError 例外が発生する
if error is CancellationError {
// キャンセルされたことが分かるように標準出力する
print("CancellationError")
}
}
}.cancellable(id: CancelID.example)
case .cancelFunction:
// 処理のキャンセルが要求された
state.status = .canceled
// 処理をキャンセルする
return .cancel(id: CancelID.example)
case .completeFunction:
// 処理の完了
state.status = .completed
return .none
}
}
enum Status: Equatable {
case initial // 初期状態
case running // suspend 関数動作中
case canceled // suspend 関数がキャンセルされた
case completed // suspend 関数が完了した
}
// 画面の状態
struct State: Equatable {
var status = Status.initial
}
// 画面の操作や非同期処理の結果
enum Action: Equatable {
case callFunction // suspend 関数を呼び出す
case cancelFunction // suspend 関数をキャンセルする
case completeFunction // suspend 関数が完了した
}
}
Reducer に対応する SwiftUI を作成
Reducer に対応する SwiftUI をこのように作成しました。
struct SuspendView: View {
let store: StoreOf<SuspendReducer>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack {
HStack {
Button(
action: {
viewStore.send(.callFunction)
}
) {
Text("関数呼び出し")
}.padding(16)
Button(
action: {
viewStore.send(.cancelFunction)
}
) {
Text("関数キャンセル")
}.padding(16)
}
switch viewStore.status {
case .initial:
EmptyView()
case .running:
Text("suspend 関数動作中")
case .completed:
Text("suspend 関数完了")
case .canceled:
Text("suspend 関数はキャンセルされた")
}
Spacer()
}.navigationBarTitle("suspend 関数動作確認")
}
}
}
見た目はこのようになっています。
SKIE を使わずにビルドした結果
まずは SKIE を使わずに Kotlin Multiplatform プロジェクトをビルドします。すでに導入済みの場合は、Gradle Configuration を変更して SuspendInterop
を無効にします。
skie {
features {
group {
co.touchlab.skie.configuration.SuspendInterop.Enabled(false)
}
}
}
アプリの「関数呼び出し」ボタンを押すと、 .callFunction
アクションが実行されますが、 try await ExampleKt.exampleFunction()
で
Calling Kotlin suspend functions from Swift/Objective-C is currently supported only on main thread
エラーが発生してクラッシュします。SKIE の Suspend Functions の解説の通り Kotlin の suspend 関数はメインスレッドからしか呼べないようです。
動作しているスレッドを print
関数で確認してみます。
switch action {
case .callFunction:
state.status = .running
print(".callFunction is on main thread: \(Thread.isMainThread)")
return .run { send in
do {
print("run is on main thread: \(Thread.isMainThread)")
try await ExampleKt.exampleFunction()
await send(.completeFunction)
} catch {
if error is CancellationError {
print("CancellationError")
}
}
}.cancellable(id: CancelID.example)
.callFunction is on main thread: true
run is on main thread: false
.run
に渡したブロックの中はメインスレッドではないようです。このままでは Kotlin の suspend 関数を TCA の Reducer では使えません。
SKIE を使ってビルドした結果
そこで SKIE の Suspend Functions 機能を有効にしてビルドします。
「関数呼び出し」ボタンを押すと、 .callFunction
アクションが実行され、 try await ExampleKt.exampleFunction()
は無事に完了します。
また try await ExampleKt.exampleFunction()
は途中でキャンセルすることができます。
キャンセルされたときに発生する例外 CancellationError
も標準出力で確認できました。
do {
// Kotlin の suspend 関数呼び出し
try await ExampleKt.exampleFunction()
await send(.completeFunction)
} catch {
// 処理がキャンセルされると、CancellationError 例外が発生する
if error is CancellationError {
// キャンセルされたことが分かるように標準出力する
print("CancellationError")
}
}
まとめ
今回は SKIE の Suspend Functions 機能について、TCA に乗せる形で動作を確認しました。Kotlin Multiplatform による共通化の範囲として、Android のアプリ推奨アーキテクチャを前提に説明すると
の2パターンがあると思います。後者の方針を採用し、ドメインレイヤの処理が Kotlin の suspend 関数で、状態ホルダーに TCA を採用した場合、問題なく Kotlin の suspend 関数を呼べてキャンセルもできることが分かりました。