Why is my child SwiftUI view not updating?

I have a simple child view that contains a text view.


Changing the state of my parent view should change that of my child, but it does not happen.


See the example code below. The Text (green) in ContentView is updated correctly when the button is pressed, but not the child view (Orange).


Why?


struct ContentView: View {
  @State var msg: String = ""
  var body: some View {
    VStack {
      Button(action: {
        self.msg = "Hallo World"
      }, label: { Text("Say Hallo")})
      
      ChildView(msg: msg).padding().background(Color.orange)
      Text(msg).padding().background(Color.green)
    }.frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

struct ChildView: View {
  @State var msg: String
  
  var body: some View {
    Text(msg)
  }
}

Accepted Reply

Your Button, defined inside ContentView, is only changing the content of the property within the ContentView. You've passed a copy of that to ChildView, so it has a separate value, stored elsewhere in memory. Thus changing ContentView.msg won't alter the value of ChildView.msg.


I suspect that if your ChildView's msg property were not an @State value, then it would be recreated. The way SwiftUI works in regard to state is this:


  1. Cleans the 'touched' state on all @State variables.
  2. Invokes body on a view.
  3. Looks at the 'touched' state on the @State variables.
  4. If a state property was accessed, it's marked, so that when that state is modified, it triggers another body call.


In your ContentView, you're accessing your local @State property, so changes to that property now trigger a redraw. In the ChildView, you're accessing a different @State property. Now, when ContentView.body is invoked a second time, it generates a view containing a new ChildView with a new value (as expected). Then SwiftUI goes to use these new values to patch up the real UI types (UILabel, etc). I suspect that, because the original ChildView is using its own @State property, and SwiftUI hasn't seen it being modified, then SwiftUI is skipping that update—after all, to SwiftUI this is 'just a view' and thus could be small and efficient or a bloated monster that takes a second to redraw. It'll do everything it can to avoid updating the view hierarchy.


So I suspect that making ChildView be a POD (Plain Old Data) type, with no @State variables, would in fact cause it to redraw, since SwiftUI has no other means of seeing the change.


Alternatively, note that the Text view is Equatable (also note: its View conformance is in an extension—which should tell you something about what Text really is). If you make your ChildView conform to Equatable then you may find that SwiftUI can now tell the difference between two instances, and will update the live view content accordingly.

  • I had the same problem, but was already using a POD in the child. The data came into the parent view via an Observable object, though. The only solution was to copy the Observable to a Bindable in the parent and hand this bindable over to the child. Then it worked. Which is strange.

Add a Comment

Replies

The reason is the way you've store the message inside your child view: you've take a copy of the string and created a second

@State
variable, referencing a separate copy of the data. What you need is to declare a single source of truth and bind other items to that. This means that there's exactly one
@State
property containing the data, and everything else references that one state property via
@Binding
properties.


Here's a reworked (and completely untested) version of your code which ought to work:


struct ContentView: View {  
  @State var msg: String = ""  
  var body: some View {  
    VStack {  
      Button(action: {  
        self.msg = "Hallo World"  
      }, label: { Text("Say Hallo")})  
        
      ChildView(msg: $msg).padding().background(Color.orange)  
      Text(msg).padding().background(Color.green)  
    }.frame(maxWidth: .infinity, maxHeight: .infinity)  
  }  
}  
  
struct ChildView: View {  
  @Binding var msg: String  
    
  var body: some View {  
    Text(msg)  
  }  
}


The required changes are on line 9 (use the

$
prefix to get a binding to the state variable) and 16 (declare the property using the
@Binding
property wrapper).
  • This approach worked in my case, although I hadn't used a State variable in the child. The original object came from an Observable, though, which was handed over from the parent to the child as a variable. This triggered no re-render on the child. After I copied the Observable variable to a Binding and handed it over like this, it worked fine. But I still don't understand why.

Add a Comment

I knew that I could do it that way, but I am trying to understand something: How is Text doing it?


If I wanted to create a custom view that can be re-used, then I would like it to have State. msg is passed to Text as "msg" and not as "$msg", which means Text does not take it as a binding.


Also, if I understand it correctly (and please correct me if I am wrong), then passing a binding means that the value can be modified. In my ChildView this is not the case, so I was reluctant to pass it as a binding.


I am still confused why I can call Text(msg) and it works, but. ChildView(msg) does not.

Your Button, defined inside ContentView, is only changing the content of the property within the ContentView. You've passed a copy of that to ChildView, so it has a separate value, stored elsewhere in memory. Thus changing ContentView.msg won't alter the value of ChildView.msg.


I suspect that if your ChildView's msg property were not an @State value, then it would be recreated. The way SwiftUI works in regard to state is this:


  1. Cleans the 'touched' state on all @State variables.
  2. Invokes body on a view.
  3. Looks at the 'touched' state on the @State variables.
  4. If a state property was accessed, it's marked, so that when that state is modified, it triggers another body call.


In your ContentView, you're accessing your local @State property, so changes to that property now trigger a redraw. In the ChildView, you're accessing a different @State property. Now, when ContentView.body is invoked a second time, it generates a view containing a new ChildView with a new value (as expected). Then SwiftUI goes to use these new values to patch up the real UI types (UILabel, etc). I suspect that, because the original ChildView is using its own @State property, and SwiftUI hasn't seen it being modified, then SwiftUI is skipping that update—after all, to SwiftUI this is 'just a view' and thus could be small and efficient or a bloated monster that takes a second to redraw. It'll do everything it can to avoid updating the view hierarchy.


So I suspect that making ChildView be a POD (Plain Old Data) type, with no @State variables, would in fact cause it to redraw, since SwiftUI has no other means of seeing the change.


Alternatively, note that the Text view is Equatable (also note: its View conformance is in an extension—which should tell you something about what Text really is). If you make your ChildView conform to Equatable then you may find that SwiftUI can now tell the difference between two instances, and will update the live view content accordingly.

  • I had the same problem, but was already using a POD in the child. The data came into the parent view via an Observable object, though. The only solution was to copy the Observable to a Bindable in the parent and hand this bindable over to the child. Then it worked. Which is strange.

Add a Comment

Thanks, changing my child view to a POD resolved the problem.