swiftuienvironmentobject

Don't understand dynamics of environment objects and view recreation


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.


Solution

  • 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 onDisappears that needs to be run, and so on.

    As for the order in which the bodys are called, it goes from parent to child. You can see this by placing some let _ = print("...") statements in the bodys.

    Side note: instead of sending objectWillChange, you can just wrap buttonText with the @Published property wrapper instead.