6

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.

7
  • 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 :) Commented Mar 15, 2021 at 11:32
  • 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. Commented Mar 15, 2021 at 12:04
  • 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)?) Commented Mar 15, 2021 at 12:22
  • 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. Commented Mar 15, 2021 at 14:09
  • 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/ecb5dfeaa7884fc0ce96178dfdd326f8 Commented Mar 16, 2021 at 9:07

3 Answers 3

3

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

Sign up to request clarification or add additional context in comments.

9 Comments

Hi @Shadowrun IIUIC, you meant view builder establishes the dependency between state variables and view's body closures. In my example code, line 8 is the only place where the state variable is associated with LabelView. So the dependency has to be created in BuildExpression(), right? But from this doc (github.com/apple/swift-evolution/blob/…), buildExpression() can only transform an expression's return value. It can't really parse an expression. So how does it know the func's parameter is state variable?
Not quite, not the view builder on its own. The SwiftUI runtime knows it's about to recompute a view by calling its body function. Before it calls that body function it sets state to say watch out for any @state getter functions called starting now, (and when function returns, stop watching state getters to associate view V with state vars called) that's how I think of it anyway
Hmm...I'm not sure what you mean. But thanks anyway.
Thanks for the update above. I think we all agree that it's State property wrapper's getter does the trick. But I suspect the details in your flow is not correct. According to your pseudo code, when ContentView's state variable chagnes, its body function should get executed. It's not true. You can run my above example code. Tap the button (note this changes state). And you wil see that only "LabelView" was printed on the console. That means when state changes, ContentView.body() isn't re-executed.
It seems that when view builder processes line 8, the currentview in your code should be VStack. In that way, your theory works :) That said, I'm afraid I can't accept your answer yet until I'm quite confident in it. Thanks.
|
0

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

9 Comments

Wow, the above example code certainly shows you know more about SwiftUI than me (I know Swift, but I have just started learning SwiftUI :) Seriously, that's very clever test. IIUIC, the key design is that initial view creation and later view update have different code path. The test result is, when tapping button, there is no output. I think it proves the hypothesis in my update. What happens is: 1) Let's assume Bool.random() returns True, counter1's getter is called and a dependency on it is added. 2) However, tapping the button update counter2, and hence ContentView.body isn't executed.
BTW, since you are doing web programming. I have a question. SwiftUI is similar to ReactJS in that both uses virtual DOM (it's virtual view hiearchy, instead of virtual DOM, in SwiftUI, but the concept is similar). Do you know if ReactJS has similar concepts or objects like @State, @Binding, @Environment, etc?
And just FYI, there are a few open source projects aiming to provide SwiftUI compatible API for web programming, like github.com/SwiftWebUI/SwiftWebUI and github.com/TokamakUI/Tokamak. So if you are curious, you may want to check out their source code to see how they implement view update dependency. I plan to do it later this year.
Good to know! In React, the sole API to trigger a re-render is an explicit call to setState(newValue) function, and that call will compare the newValue against oldValue to see if any update is necessary. So there isn't any getter/setter tracking.
However, this tracking technique is used in other framework. Like Vue.js use the same idea like SwiftUI. And svelte does approximately the same but with compile time analysis, not tracking in runtime.
|
0

I've done some additional testing. And found that even when a subview has no link to a state var, it may be redrawn as soon as some var inside changes.

Here is an example:

struct ContentView: View {
    @State var mydata1:Int = 0
    @State var mydata2:Int = 1
    var body: some View {
        VStack {
            Spacer()
            Text("Hello world \(mydata1)")
            MyView1(v1:$mydata1, v2:$mydata2)
            Text(" myData1 = \(mydata1) myData2 = \(mydata2) ")
            Spacer()
            MyView2()// no Binding to either (v1:$mydata1, v2:$mydata2)
            Text("Bye bye \(mydata1)")
            Spacer()
        }
    }
}

struct MyView1:View {
    @Binding var v1:Int
    @Binding var v2:Int
    var body: some View {
        Text("MyView1")
        Button(action: {
             v1 += 10
        }, label: {
             Text("OK")
        })
    }
}

struct MyView2:View {
    let rand = (0...100).randomElement() // Without this, print will not trigger again
    var body: some View {
        print(100) // don't need to print(rand)
        return (Text("MyView2"))
    }
}

1 Comment

it's because the MyView2 instance value changed. SwiftUI works by comparing virtual view hierarchies. If a view's value changes, it re-renders it by calling its body. There is protocol used for this purpose (checking if a view's value changes), though I can't recall it off the top of my mind.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.