Can't observe the same viewModel object in two different screens on iOS
cameron-greene-telus opened this issue · 5 comments
I'm using Koin DI to inject singleton ViewModels into my iOS app - I have the following LoginScreen defined:
struct LoginScreen: View {
@ObservedViewModel var viewModel: LoginViewModel = Koin.instance.get()
...
}
I created a SplashScreen that also depends on the LoginViewModel, but this code fails with the error kotlin.IllegalStateException: KMMViewModel can't be wrapped more than once
struct SplashScreen: View {
@ObservedViewModel var loginViewModel: LoginViewModel = Koin.instance.get()
...
}
If I make the ViewModel manually (so it's not the same object as the one in LoginScreen), it doesn't get the error when run, but loses the benefit of being a singleton between the two screens.
struct SplashScreen: View {
@ObservedViewModel var loginViewModel: LoginViewModel = LoginViewModel(authRepository: Koin.instance.get())
...
}
There are a couple options here - I could pass the ViewModel from one screen to the next, but that tightly couples one view to the one before it, which I don't want. I could also create a SplashScreenViewModel
and use that, but there are other places in my app that are going to need to share instances of the same ViewModel, so this would become an issue again very quickly.
This is not an issue on Android, you can inject a singleton ViewModel into as many screens as you want without getting this error.
Is this something that could be fixed at the library level, or is there another way to access singleton ViewModels in multiple screens?
Is this something that could be fixed at the library level, or is there another way to access singleton ViewModels in multiple screens?
Singleton ViewModels in itself are supported. However the issue/challenge is in storing a reference to this singleton.
On iOS your KMMViewModel is being wrapped inside an ObservableViewModel
.
The @*ViewModel
property wrappers transparently create or get this wrapper and keep a reference to it.
In cases like these where the ViewModel outlives the view you can use the observableViewModel(for:)
function.
It returns the ObservableViewModel
wrapper which you can store anywhere you like.
E.g. you could store this wrapper in a parent view as a regular property, or in your DI graph as a singleton.
Note: keep in mind that this wrapper keeps a strong reference to your KMMViewModel, so storing a reference to the wrapper inside the viewmodel would result in a reference cycle.
Another approach could be to provide these singleton viewmodels as @EnvironmentViewModel
s.
That would be similar to storing the wrapper in a parent view, but would reduce the amount of boilerplate.
@rickclephas thanks for the quick response! Android dev here trying to wrap my brain around the iOS side of things, so I appreciate the help.
Assuming I have a ContentView that creates the LoginViewModel
object and passes it to SplashScreen
and LoginScreen
as an environment variable, what might a minimum example of each file look like?
Right now I have the following:
struct ContentView: View {
var loginViewModel: LoginViewModel = Koin.instance.get()
var body: some View {
SplashScreen()
.environmentObject(loginViewModel)
}
}
struct SplashScreen: View {
@EnvironmentViewModel var loginViewModel: LoginViewModel
var body: some View {
if (viewModel.state.isLoggedOut) {
LoginScreen()
}
...
}
}
struct LoginScreen: View {
@EnvironmentViewModel var viewModel: LoginViewModel
...
}
and the app crashes on launch with the error:
SwiftUI/EnvironmentObject.swift:90: Fatal error: No ObservableObject of type ObservableViewModel<SharedLoginViewModel> found. A View.environmentObject(_:) for ObservableViewModel<SharedLoginViewModel> may be missing as an ancestor of this view.
What am I missing here?
Changing environmentObject(loginViewModel)
to environmentViewModel(loginViewModel)
should do the trick.
That did it - thank you for your help!