Is there a way to scope a `ViewModel` to a SwiftUI...
# multiplatform
e
Is there a way to scope a
ViewModel
to a SwiftUI
View
? KMM-ViewModel does it with the
@StateViewModel
annotation, but is there a way when only using the Google libraries?
r
That depends on what you mean by “scope”. You can use
@State
to store a regular object as a state value in your SwiftUI view. However that only stores the object, it won’t observe state changes nor will it handle the lifecycle of the viewmodel.
e
I was wondering about the lifecycle handling, all the way to
onCleared()
and coroutine cancellation. Not that I’m not content with KMM-ViewModel, for which I’m very grateful, btw 😁 So I guess the multiplatform support is meant more for use in composables.
r
Yeah as far as I know there is no first party integration with SwiftUI, so far the multiplatform support is availability for other targets only.
m
Actually, the Jetpack ViewModel is not meant to use directly in composables. It can't follow the lifecycle of an individual composable function. In fact we always scope ViewModels to some higher level components like Navigation Destination, Fragment or Activity. You can achieve the same on iOS side when you don't use SwiftUI navigation, but you wrap your SwiftUI views in UIControllers instead. There is a
UIHostingController
available in the SwiftUI. In our KMP projects we extend it to handle the lifecycle of ViewModel. For this purpose we use the
ViewModelStore
which is available in multiplatform same as the
ViewModel
itself. When we create an instance of this controller we add given ViewModel to the store and when this instance is getting destroyed we call
viewModelStore.clear()
which clears the
ViewModel
and underlying
CoroutineScope
attached to it.
Copy code
class UIHostingControllerWrapper<V: View, VM: ViewModel> : UIHostingController<V> {
    private let viewModelStore = ViewModelStore()
    
    init(rootView: V, viewModel: VM) {
        super.init(rootView: rootView)
        let key = String(describing: VM.self)
        viewModelStore.put(key: key, viewModel: viewModel)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("Not implemented")
    }
    
    deinit {
        viewModelStore.clear()
    }
}
Probably you can achieve something similar in SwiftUi by scoping ViewModels to navigation destinations, not views, same as Compose does. As far as I see the
navigationDestination
modifier in SwiftUI has
isPresented
binding. It might be a good idea to clear the ViewModel when this value is set back to
false
. However I can't offer you a ready solution right now, as our iOS devs prefer to use UIKit navigation instead of SwiftUI so I didn't experiment with it yet 😅
e
That’s an interesting direction to explore, thank you!
kodee loving 1
@Marcin Piekielny Here’s my very primitive solution, based on yours: a wrapper class for the
ViewModelStore
with a
deinit()
implementation that, like yours, simply calls
clear()
on the wrapped
ViewModelStore
. The
View
creates a wrapper and passes its `ViewModel`s to the store. When the
View
is taken off the navigation stack, it’s destroyed and the wrapper with it, calling
deinit()
.
Copy code
class JetpackVMStoreWrapper {
    private let store = ViewModelStore()
    
    func put<VM: ViewModel>(_ vm: VM) {
        store.put(key: VM.self.description(), viewModel: vm)
    }
    
    deinit {
        store.clear()
    }
}
Copy code
struct WhateverScreen: View {
    private let vmStore = JetpackVMStoreWrapper()
    
    private let vm = WhateverViewModel()
    
    init() {        
        vmStore.put(self.vm)
    }
    
    var body: some View {
        EmptyView()
    }
}
m
Nice! Looks pretty well. Maybe it could be even possible to implement some base
View
class which handles this
vmStore.put
part
e
Of course, can probably add some utility functions just like the various
viewModel()
on the android 10 side
m
Very promising, good to know that this
deinit
works as expected for objects inside the
View
139 Views