I've created a simplified version of a problem I'm seeing in a more complex app. So rather than suggest that I rewrite the code, can you help me understand why things don't work as I expect? There are reasons the code is structured in this odd way to be similar to the more complex case. Here's the simplified test code - the top level view is TestEnvUpdate:
class GlobData: ObservableObject {
var buttonText: String? = "hello"
static let shared = GlobData()
func setText(str: String?) {
buttonText = str
objectWillChange.send()
}
}
struct TestEnvUpdate: View {
var body: some View {
ParentView().environmentObject(GlobData.shared)
}
}
struct ParentView: View {
@EnvironmentObject var globData: GlobData
var body: some View {
if globData.buttonText != nil {
ChildView()
} else {
Text("buttonText is nil")
}
}
}
struct ChildView: View {
@EnvironmentObject var globData: GlobData
var body: some View {
VStack {
Text(globData.buttonText!) // error occurs here
Button ("set to nil") { globData.setText(str: nil) }
}
}
}
When I press the "set to nil" button, the GlobData setText function runs, and the objectWillChange.send() function runs. I would then expect ParentView body function to run, notice that buttonText is set to zero, and not attempt to create the ChildView, since ParentView depends on the environment object. But instead the ChildView runs and crashes because buttonText is nil.
Can anyone shed any light on why this is happening, and what the sequencing works like with objectWillChange.send()? Are there any threading concerns or any deferred operations when using objectWillChange.send()?
I have a feeling the answer to this question will deepen my understanding of the way that SwiftUI sequences the creation of views in a view hierarchy.
globData
is a dependency of ChildView
. When SwiftUI observes a change in globData
(because you called objectWillChange.send()
), it needs to call ChildView.body
to see what has changed.
Yes, ChildView
is going to disappear anyway, because of the if
in ParentView
, but that doesn't matter. There might be changes in ChildView
that needs to be animated, or onDisappear
s that needs to be run, and so on.
As for the order in which the body
s are called, it goes from parent to child. You can see this by placing some let _ = print("...")
statements in the body
s.
Side note: instead of sending objectWillChange
, you can just wrap buttonText
with the @Published
property wrapper instead.