1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Kotlin 2.4.0のSwift Exportにより、Kotlinの Flow がSwift側から扱えるようになりました。Flow.asAsyncSequence() により、Swiftの AsyncSequence として処理できます。

この記事では、SwiftUIから気軽に Flow を参照し、描画に使える状態として扱えるようにする方法を紹介します。

サンプルに利用するコード

本記事では以下のような状態を管理する Store クラスをKotlinで定義し、Swift ExportによりSwift側から参照することを前提に説明します。

class Store {
    private val _state = MutableStateFlow(0)
    val state = _state.asStateFlow()

    fun increment() {
        _state.update { it + 1 }
    }
}

Composeではどのように扱っているか

SwiftUIでの扱いを考える前に、Composeではどうしていたかをおさらいします。Composeでは collectAsState() により、Flow / StateFlowState に変換しています。

@Composable
fun ContentComposable() {
    val store = remember { Store() }
    val state by store.state.collectAsState()
    // ...
}

Kotlinの実装を ObservableObject でラップする

SwiftUIの View は、画面の再描画のたびに init が呼び出されるという特性があります。そのため、View の中で直接Kotlinのクラスをインスタンス化すると、不要な再生成が繰り返されてしまいます1

これを防ぐ、Composeの remember のように「Viewのライフサイクルに合わせて一度だけ初期化・保持」させるようなものを実装します。具体的には、SwiftUIの @StateObject で扱えるようシンプルなラッパークラスを作成します。

final class Remember<T>: ObservableObject {
    let value: T

    init(_ initialValue: @autoclosure () -> T) {
        self.value = initialValue()
    }
}

Flow の観測・値を用いた描画のための View を実装

SwiftUIとComposeではインスタンスの作成タイミングやライフサイクルの管理方法が異なります。SwiftUIではComposeと同じアプローチをとることはできません。

そこで、SwiftUIの @State で状態を保持し、inittask にて初期化と更新を行う専用の View を作成します。

SwiftではKotlinの Flow を直接 collect することはできませんが、Swift Exportによる Flow.asAsyncSequence() により AsyncSequence に変換し、for try await することで collect と同等の動作を実現できます。

struct ObservingView<T, Content: View>: View {
    @State private var state: T
    private let flow: any KotlinTypedFlow<T>
    private let content: (T) -> Content

    init(
        flow: any KotlinTypedFlow<T>,
        initialValue: T,
        @ViewBuilder content: @escaping (T) -> Content
    ) {
        self._state = State(wrappedValue: initialValue)
        self.flow = flow
        self.content = content
    }

    init(
        stateFlow: any KotlinTypedStateFlow<T>,
        @ViewBuilder content: @escaping (T) -> Content
    ) {
        self._state = State(wrappedValue: stateFlow.value)
        self.flow = stateFlow
        self.content = content
    }

    var body: some View {
        content(state)
            .task {
                do {
                    for try await value in flow.asAsyncSequence() {
                        state = value
                    }
                } catch is CancellationError {
                    // cancelled
                } catch {
                    assertionFailure("Flow<\(T.self)> collection failed: \(error)")
                }
            }
    }
}

SwiftUIでの利用例

作成した RememberObservingView を使って、実際にSwiftUIの View を構築します。

View では直接的には @StateObject を用いて Store を保持することしかしません。状態の監視等は ObservingView に閉じて行います。ObservingViewcontent では state を受け取ることができるため、Flow の変更をUIに反映することができます。

struct ContentView: View {
    @StateObject private var storeHolder = Remember(Store())

    var body: some View {
        let store = storeHolder.value
        ObservingView(stateFlow: store.state) { state in
            VStack {
                Text("\(state)")
                Button("increment") {
                    store.increment()
                }
            }
        }
    }
}

結果

前節までで紹介したサンプルコードにより、Flow をUI上で表示できるようになります。

SwiftUI上でFlowの表示ができている様子

まとめ

  • Kotlin 2.4.0のSwift Exportと Flow.asAsyncSequence() を活用することで、Swiftからも Flow を扱うことができる
  • SwiftUIのライフサイクルの違い(init の反復呼び出しなど)を吸収するため、ObservableObject / @StateObject を活用してKotlin側のインスタンスを保持する
  • Flow の購読処理を ObservingView のように専用の View へ切り出すことで、Flow を手軽に描画に利用できる。

参考文献

  1. @Observable@State を使わずに、旧来の ObservableObject@StateObject を使う理由はここにあります。ただし、Xcode 27からは @State 周辺の仕組みが変わるため、今後は実装方法が変わると思います。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?