简体   繁体   中英

How SwiftUI detects there are @State variable values passed to subviews?

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 both ContentView 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.

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

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)
    ])
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM