0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SKIE flows in swiftUI

Posted at

いつの間にやら、Kotlin側のViewModelのFlowをSwiftUIで簡単に監視できるようになってた。

struct ExampleView: View {
    let viewModel = SharedViewModel()

    var body: some View {
        // Observing multiple flows with a "initial content" view closure.
        Observing(viewModel.counter, viewModel.toggle) {
            ProgressView("Waiting for counters to flows to produce a first value")
        } content: { counter, toggle in
            Text("Counter: \(counter), Toggle: \(toggle)")
        }
    }
}

純粋にKMPでKotlinのViewModelをSwiftで使うには

ViewModelを共通で使うってケースを考える。

class CounterViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UIState<CounterState>>(UIState.Loading)
    val uiState: StateFlow<UIState<CounterState>> = _uiState.asStateFlow()
    
    init {
        loadInitialState()
    }
    
    private fun loadInitialState() {
        viewModelScope.launch {
            _uiState.update { UIState.Loading }
            delay(1500) // Loading simulation
            _uiState.update { 
                UIState.Content(
                    CounterState(
                        label = "Counter",
                        count = 0
                    )
                )
            }
        }
    }
    ...

SwiftからするとViewModelはObservableObjectではないので、以下のようなAdapter的な層が必要になる。

class CounterViewModelAdapter : ObservableObject {
    private let ktViewModel = CounterViewModel()
    @Published var swiftUiState: SwiftUIState<CounterState, Error> = .loading

    @MainActor
    func activate() async {
        Task {
            for await state in ktViewModel.uiState {
                self.swiftUiState = switch onEnum(of: state) {
                case .loading:
                    .loading
                case .content(let content):
                    if let data = content.data {
                        .content(data)
                    } else {
                        .error(NSError(domain: "No data", code: -1))
                    }
                case .error(let error):
                    .error(NSError(domain: error.message, code: -1))
                }
            }
        }
    }
    ...ktViewModel側と同じ関数の公開

KotlinのFlow は SKIEによって AsyncSequence に変換される。
その値を一度取り出して 上記のようにマッピングする必要がある。

また、以下のようなありがち?な、直和な構造もSwiftからは使いにくい。

sealed class UIState<out T> {
    object Loading : UIState<Nothing>()
    data class Content<T>(val data: T) : UIState<T>()
    data class Error(val message: String, val throwable: Throwable? = null) : UIState<Nothing>()
}

KotlinのNothingは、SwiftではKotlinNothingというクラスになる。これはKotlinと違い 全ての型のsub type とはなっていないので、
@Published var uiState: UIState<CounterState> = UIStateLoading() のような代入は型が合わずコンパイラに弾かられる。
要するに型の共変,反変が 吐き出されたSwiftコードでサポートされていない(し、swiftでもサポートはない?)

enum SwiftUIState<Content, Failure: Error> {
    case loading
    case content(Content)
    case error(Failure)
}

Swift側にも似た構造を作って 適切にマッピングが必要

UI的には SwiftのViewModelを見るような形になる。

struct LegacyContentView: View {
    @ObservedObject private var viewModel = CounterViewModelAdapter()
    var body: some View {
        LegacyCountScreen(
            uiState: viewModel.swiftUiState,
            onIncrement: { viewModel.increment() },
            onDecrement: { viewModel.decrement() },
            onReset: { viewModel.reset() },
            onRetry: { viewModel.retry() }
        ).task { await viewModel.activate() }
    }
}
// 略

flows in swiftUI

SKIEのflows in swiftUIを有効にすると、上記の例はシンプルにこれだけで済む。

import SwiftUI
import Shared

struct ContentView: View {
    private let viewModel = CounterViewModel() // kotlin側のViewModel
    
    var body: some View {
        Observing(viewModel.uiState) { uiState in
            CounterScreen(
                uiState: uiState,
                onIncrement: { viewModel.increment() },
                onDecrement: { viewModel.decrement() },
                onReset: { viewModel.reset() },
                onRetry: { viewModel.retry() }
            )
        }
    }
}

kotlin側のViewModelをそのまま使って、ObservedObject的に使える。
前述のUiStateの型が合わない問題も面倒見てくれる。

終わり

Kotlin側のViewModelを Observableにしてくれるライブラリもあるのでこいつを使うというのもある。
https://github.com/rickclephas/KMP-ObservableViewModel

とはいえ、SKIEは Swift Kotlinを統合していれば使っていることが多いと思うので SKIEに入っているのはうれしい。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?