Reputation: 1127
When a State
variable in a parent View changes, SwiftUI will redraw child Views and consequently call their constructors again.
Consider the following View:
struct Parent: View {
@ObservedObject var viewModel: ParentViewModel
var body: some View {
VStack {
Child(viewModel: ChildViewModel())
}
}
}
Any state change in the Parent
View (ex. a Published
variable changing in its ViewModel) might cause the Child
View to be redrawn, leading to its viewModel
property to receive a new instance of ChildViewModel
.
When a subview gets redrawn I expect it to still reflect the state it was previously in, however since constructor is called again with a new instance of the ViewModel
, this is not the case.
Let me preface this by stressing that we use SwiftUI 1.0, so using the @StateObject
property wrapper is not an option.
We've attempted to use a ViewModelProvider
class that caches ViewModels of a certain type for a longer time, so that consecutive initializations of a certain ViewModel will return the same instance. This works perfectly if there can only be one instance of a ViewModel (and thus, View) at one time. However, once you add Views that can navigate to new instances of that same View (adding the requirement for multiple ViewModels of the same type to exist) this falls apart.
We have some additional ideas about making aforementioned pattern work, but I just can't shake the feeling there should be a better way to do this without jumping through hoops.
Setting Example
as the rootView
of your SceneDelegate
, combined with some furious tapping of the button, should trigger print statements that should show you that every now and then (not even predictably) a different instance of ChildViewModel
is used.
struct Example: View {
@State var selection = 0
var body: some View {
VStack {
Button("Tapped \(selection) times") {
self.selection += 1
}
ChildView(viewModel: ChildViewModel())
ChildView(viewModel: ChildViewModel())
}
}
}
struct ChildView: View {
@ObservedObject var viewModel: ChildViewModel
var body: some View {
Text("Child")
}
}
class ChildViewModel: ObservableObject {
init() {
withUnsafePointer(to: self) {
print($0)
}
}
}
Upvotes: 2
Views: 710
Reputation: 54426
Use a StateObject
- it is only created once:
struct ExampleView: View {
@State var selection = 0
var body: some View {
VStack {
Button("Tapped \(selection) times") {
self.selection += 1
}
ChildView() // don't pass the ViewModel
ChildView()
}
}
}
struct ChildView: View {
@StateObject var viewModel = ChildViewModel() // create here
var body: some View {
Text("Child")
}
}
Also see:
You may need to use an @EnvironmentObject
instead.
In the SceneDelegate
:
let viewModel = ChildViewModel()
ExampleView()
.environmentObject(viewModel)
and in the ChildView
:
struct ChildView: View {
@EnvironmentObject var viewModel: ChildViewModel // automatically injected
var body: some View {
Text("Child")
}
}
Alternatively, you can use a struct instead of a class:
struct ChildViewModel { ... }
struct ChildView: View {
@State var viewModel = ChildViewModel() // mark as `@State`
var body: some View {
Text("Child")
}
}
Upvotes: 1