Below is a simple example which doesn't use @Binding
, but pass @State
variable value directly to subviews (through closure).
1 import SwiftUI
2 struct ContentView: View {
3 @State var counter = 0
4 var body: some View {
5 print("ContentView")
6 return VStack {
7 Button("Tap me!") { self.counter += 1 }
8 LabelView(number: counter)
9 }
10 }
11 }
12 struct LabelView: View {
13 let number: Int
14 var body: some View {
15 print("LabelView")
16 return Group {
17 Text("You've tapped \(number) times")
18 }
19 }
20 }
Tapping the button modifies @State
variable, which then causes bothContentView
and LabelView
get updated. A common explanation is that, because the toplevel view's @State
variable changes, so the toplevel ivew's body is re-executed.
But my experiments show that it's not that simple.
For example, if I remove the
LabelView(number: counter)
call (see line 8 above) in the code, then tapping the button won't get ContentView.body() executed.For another example, if I pass
@Binding
variable, instead of@State
variable value to subview, then tapping the button won't get ContentView.body() executed either.
So it seems that SwiftUI knows that if toplevel view's @State
variable value is passed to subviews. I wonder how it determines that? I know compiler has this kind of information. But SwiftUI is a framework written in Swift. I don't think Swift has this kind of feature.
While writing this question, I realize one possible way to do it. Note the above code is executed when creating initial view hierarchy. That is, if a @State
variable is passed to a subview, its getter must get executed before view update. It seems SwiftUI might take advantage of this to determine if there are @State
variables is passed to a subview (or accessed in toplevel view's body).
Is that the way how it works? I wonder how you guys understand it? Thanks.
Update 1:
Below is my summary of the discussion with @Shadowrun, who helped to put the pieces together. I don't know if it's really what happens under the hood, but it seems reasonable to me. I write it down in case it helps others too.
SwiftUI runtime maintains view hiearchy somehow. For example,
For subviews, this may be done with the help of viewbuilders. For example, viewbuilders registers subviews with the runtime.
For toplevel view, this may be done by runtime (note toplevel view's body is a regular computed property and not transformed by viewbuilder).
SwiftUI can detect if state variable is read or written through its getter/setter.
This is hypothesis. When SwiftUI creates the initial view hiearchy, it does the following for each view:
Set current view
Run the view's body code (I find that body of layout types like VStack has
Never
type. I suppose it has an equivalent way to iterate through its subviews).If SwiftUI detects that the view's state variable is read, it save the dependency between that state variable and the view somewhere. So if the app modifies that state variable later, the view's body will be executed to update the view.
Scenario 1: If the view body doesn't read the view's state variable, it won't trigger the above mechanism. As a result, the view won't be updated when the app modifies the state variable.
Scenario 2: If the view pass its state variable to its subview through binding (that is, the state variable's projectedvalue), it won't trigger the above mechanism either, because reading/writing a state variable's projectedvalue is through Binding property wrapper's getter/setter, not State property wrapper's getter/setter.
Update 2:
An interesting detail. Note the +=
operator in line 7. It's usually implemented as reading and writing. So the button handler in line 7 read state variable also. However, button handler isn't called when creating initial view hierarchy, so based on the theory above SwiftUI won't recall ContentView.body
just because of line 7. This can be proved by removing line 8.
Update 3:
I have a new and much simpler explanation to the original question. In my original question I asekd:
So it seems that SwiftUI knows that if toplevel view's @State variable value is passed to subviews. I wonder how it determines that?
It's a wrong question. It's very likely that SwiftUI doesn't really knows that the state is passed to subview. Instead it just knows that the state is accessed in the view's body(perhaps using a similar mechanism I described above). So, if a view's state changes, it reevaluates the view's body. Just this simple rule.
LabelView(number: counter)
inbody
of ContentView - LabelView will be drawn - just because it is part of the ContentView.body
will be called whenever the state changes. While there is actually a lot of "magic" in SwiftUI, in this case I think there is none :) – CouchDeveloperContentView.body
is not executed even though state variable changes. – rayxLabelView(number: counter)
, it seems when not referencingcounter
, property wrapper@State
will not create subscriptions under the hood, and thusbody
will not be called. Somewhere in WWDC Introducing Swift UI it is stated that "One of the special properties of@State
variables is that SwiftUI can observe when they're read and written.". This supports the above assumption. Going to the source code would reveal the magic. (what happens if you just addprint(counter)
?) – CouchDeveloperprint(counter)
causesContentView.body
executed (of course). So you think@State
is implementing based on combine? If so, the subscription needs to be set up ahead, and hence it should be possible to get the dependency between state varibles and subviews. However, I doubt if@State
is really implemented using combine. I don't see there is any need for that, because swiftui can easily observe state variables' read and write through State property wrapper's getter and setter. – rayxcounter
. Unfortunately, Apple's Swift UI is not open source, but there is an attempt to reimplement exactly this peace of mechanic for the purpose of figuring out how this might work: gist.github.com/AliSoftware/ecb5dfeaa7884fc0ce96178dfdd326f8 – CouchDeveloper