Hello people of multiplatform. I have a question h...
# multiplatform
j
Hello people of multiplatform. I have a question hoping someone can give me some insight on how I might be able to handle SharedFlow Events within SwiftUi (my iOS skills are rather poor currently). I have a sample app that is working as expected on the Android side. I am using the Android ViewModel that supports KMP and SKIE on the iOS side. I have a SharedFlow within my viewmodel below that handles one time events.
Copy code
val _event = MutableSharedFlow<UiEvent>()
val event: SharedFlow<UiEvent>
    get() = _event
sealed class UiEvent {
    data object DisplayToastEvent : UiEvent()
    data object NavigateToDetailEvent : UiEvent()
}
from my composable I handle my event below
Copy code
LaunchedEffect(Unit){
    viewModel.event.collect { event -> 
        is UiEvent.NavigateToDetailEvent -> //navigate to detail
        is UiEvent.DisplayErrorMsgEvent -> //display toast msg
    }
}
What/Where is the best way to handle these Events from SwiftUi? Is there an equivalent in SwiftUi? I am having no problem observing my StateFlow and updating the view when necessary but cant seem to find any information regarding one off events. Thanks for the help.
@Ryan Simon
c
Last year, I decided to implement my own StateFlow exclusively for the iOS platform. That custom module is used to my view models, which I share between the two platforms.
I understand that there may be a better solution these days. At the time of implementation, I do not believe the iOS platformed liked kotlin's StateFlow. I didn't care to further
I see that MutableStateFlow is an interface. There is a chance you may be able to expose it
I'll share with you my source code
Copy code
interface CFlow<T> {
    fun tryEmit(state: T?)
    suspend fun collect(function: (T?) -> Unit)
    suspend fun unCollect()
    val value: T?
}

expect fun <T> getCFlow(value: T?): CFlow<T>
With the keyword being "expect"
Implementation for the Android Platform:
Copy code
class AndroidCFlow<T>(value: T?) : CFlow<T> {
    private val _stateFlow: MutableStateFlow<T?> =
        MutableStateFlow(value)
    val stateFlow: Flow<T?> = _stateFlow

    override fun tryEmit(state: T?) {
        _stateFlow.tryEmit(state)
    }

    override suspend fun collect(function: (T?) -> Unit) {
        _stateFlow.collect { value -> function(value) }
    }

    override suspend fun unCollect() { }
    override val value: T?
        get() = _stateFlow.value

}

actual fun <T> getCFlow(value: T?): CFlow<T> = AndroidCFlow(value)
j
My stateflow is working as expected both android and iOS side
c
Oh... SharedFlow. I misread
I think SharedFlow is a StateFlow
I must review the implementation, and launch my iOS project
I have done this
j
Yes it acts the same and functions correctly. It's just the issue of how can I replicate the launched effect code iOS side to handle one off events. Navigation, error messages, etc
c
aaahhhh
you know: You may have options
because the iOS life cycle is different from the android platform
Can I assume you have used SwiftUI over there?
j
Yes using SwiftUi
c
I'm making some assertions with this part:
Copy code
launched effect code iOS side to handle one off events.
I'll share my source code and go backwards. I hope this will help.
In my case, this source code applies to the login screen
j
Ok thank you, much appreciated
c
At the time of initialization, I register a callback block:
Copy code
viewModel.viewModel.cFlow.collect(function: { latestState in           onStateReceived(latestState: latestState as! LoginViewModelStore.State)
        }, completionHandler: { error in
            print("LoginView: error=[\(String(describing: error))]")
        })
The implementation of that onStateReceived() is essentially a state driven, if that makes any sense:
Copy code
private func onStateReceived(latestState: LoginViewModelStore.State) {
        if let _ = latestState as? LoginViewModelStore.StateCurrentState {
            onCurrentState(latestState: latestState as! LoginViewModelStore.StateCurrentState)
        } else if let _ = latestState as? LoginViewModelStore.StateIdle {
            onIdle()
        }
    }
I implemented a MVI design pattern (Model-View-Intent)
I realize your situation could be two-folded
First... I implemented an object that stores instances of my published properties
Copy code
class SelectionStore: ObservableObject {
    @Published var selection: String?
}
This is how I control navigation
My choice of design pattern to handle navigation might be different from yours. I followed the VIPER architecture.
That SelectionStore that I shared, leads back to the ContentView.swift, where I determine which screen to show
var body: some View { switch selectionStore.selection { case "OnBoarding": OnBoardingView(selectionStore: selectionStore, mainPreferences: mainPreferences) case "Register": RegisterView(selectionStore: selectionStore) case "Main": MainView(selectionStore: selectionStore) case "Profile": ProfileView(selectionStore: selectionStore) case "Dashboard": DashboardView(selectionStore: selectionStore) default: LoginView(selectionStore: selectionStore) } }
I think the key in your situation will be finding ways to expose those published variables so that you can respond.
Copy code
@Published var yourProperty: String?
Without knowing too much, I'm making an assertion that your resolution is only a few lines of code
One more thing...
I'm assuming that you've already defined a few binding objects
And the content view
This is how I chose to handle error handling and navigation
heads up: This is an older implementation
I actually have changes coming at my end because I've changed the way inject dependencies into my KMM modules
j
Thanks Michael. I'll take a look at these and see if it works for my case
👍 1
c
I'm happy that I provided a grain of a salt option. I've been quiet for a while. I must say Hi to everyone. 🙂