2
votes

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.

  1. 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).

  2. SwiftUI can detect if state variable is read or written through its getter/setter.

  3. 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.

2
I understand it this way: when you call the ctor LabelView(number: counter) in body 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 :)CouchDeveloper
Hi @CouchDeveloper, your explanation is the "common explanation" I mentioned in my question. But it's not really that simple. Please see the two examples I gave in my question where ContentView.body is not executed even though state variable changes.rayx
When you remove LabelView(number: counter) , it seems when not referencing counter, property wrapper @State will not create subscriptions under the hood, and thus body 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 add print(counter)?)CouchDeveloper
Adding print(counter) causes ContentView.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.rayx
SwiftUI is based on Combine. The "magic" happens in the property wrappers. There's actually quite lot of code involved in that seemingly simple statement that references counter. 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/ecb5dfeaa7884fc0ce96178dfdd326f8CouchDeveloper

2 Answers

3
votes

When you use the state property wrapper SwiftUI can know when a variable is read, because it can manage access via custom getter function. When a view builder closure is being evaluated, SwiftUI knows which builder function it’s evaluating and can set its state such that any state variables read during that evaluation are then known to be dependencies of the view currently being evaluated.

At runtime: the flow might look like this:

Need to render ContentView, so:
  set currentView = ContentView
  call each function in the function builder...
  ...LabelView(number: counter) - this calls the getter for counter

Getter for counter is:
  associate <currentView> with counter (means any time counter changes, need to recompute <currentView>)
  return value of counter


...after ContentView function builder
 Current view = nil

But with a stack of current views...

0
votes

NOT AN ANSWER

I’m a web guy, not familiar with swift, but came across this post and find it interesting. I’m curious to see how would this piece of code behave? Think this would test your theory:

import SwiftUI
   
struct ContentView: View {
    @State var counter1 = 0
    @State var counter2 = 0
    var body: some View {
        print("ContentView")
        if Bool.random() {
            return VStack {
                Button("Tap me!") { self.counter2 += 1 }
                LabelView(number: counter1)
            }
        } else {
            return VStack {
                Button("Tap me!") { self.counter1 += 1 }
                LabelView(number: counter2)
            }
        }
    }
}

To answer your question:

Do you know if ReactJS has similar concepts or objects like @State, @Binding, @Environment, etc?

To be clear, there's no implicit getter/setter tracking in ReactJS that automatically triggers a view update (aka re-render). The sole API to trigger a re-render is an explicit call to setState() function.

However there're features comparable to SwiftUI's @State and @Binding in React. In short, @State corresponds to this.state and @Binding corresponds to this.props.

A typical React "class component" looks like this:

class CounterView extends React.Component {
  // `constructor` is equiv to `init` in swift
  constructor(props) {
    // this calls the constructor of base class `React.Component`
    // which would assign `this.props = props`
    // where as `props` is a struct passed-in from outside, most likely from parent component.
    super(props)

    // `this` keyword is equiv to `self` in swift
    // in SwiftUI you declare `@State counter` property right on the instance
    // in React you need to put `counter` property inside a nested `this.state` struct
    this.state = {
      counter: 0
    }
  }

  render() {
    return React.createElement("div", null, [
      // in SwiftUI you need to declare `@Binding heading`
      // in React, because JS is dynamic lang,
      // you can simply access any properties of the passed-in `this.props` struct
      React.createElement("h1", null, this.props.heading),
      React.createElement("button", {
        // you modify local state by calling `setState` API
        onClick: () => this.setState({ counter: this.state.counter + 1 })
      }, "Tap me!"),
      React.createElement("span", null, this.state.counter)
    ])
  }
}