【问题标题】:How SwiftUI detects there are @State variable values passed to subviews?SwiftUI 如何检测到有@State 变量值传递给子视图?
【发布时间】:2021-06-12 13:27:38
【问题描述】:

下面是一个简单的示例,它不使用@Binding,而是将@State 变量值直接传递给子视图(通过闭包)。

 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  }

点击按钮会修改@State 变量,这会导致ContentViewLabelView 都得到更新。一个常见的解释是,因为顶层视图的@State变量发生了变化,所以顶层ivew的body被重新执行了。

但我的实验表明,事情并没有那么简单。

  • 例如,如果我在代码中删除了LabelView(number: counter) 调用(参见上面的第 8 行),那么点击按钮将不会执行 ContentView.body()。

  • 再举个例子,如果我将 @Binding 变量而不是 @State 变量值传递给子视图,那么点击按钮也不会执行 ContentView.body()。

所以 SwiftUI 似乎知道顶级视图的 @State 变量值是否传递给子视图。我想知道它是如何确定的?我知道编译器有这种信息。但是 SwiftUI 是一个用 Swift 编写的框架。我认为 Swift 没有这种功能。

在写这个问题时,我意识到了一种可能的方法。注意上面的代码是在创建初始视图层次结构时执行的。也就是说,如果将@State 变量传递给子视图,则它的getter 必须在视图更新之前执行。似乎 SwiftUI 可能会利用这一点来确定是否有 @State 变量被传递给子视图(或在顶层视图的主体中访问)。

这就是它的工作方式吗?不知道大家是怎么理解的?谢谢。


更新 1

以下是我与@Shadowrun 的讨论摘要,他帮助将这些部分组合在一起。我不知道这是否真的发生在幕后,但对我来说似乎是合理的。我把它写下来以防它也帮助别人。

  1. SwiftUI 运行时以某种方式维护视图层次结构。例如,

    • 对于子视图,这可以在视图构建器的帮助下完成。例如,视图构建器向运行时注册子视图。

    • 对于顶级视图,这可以由运行时完成(注意顶级视图的主体是常规计算属性,而不是由视图构建器转换)。

  2. SwiftUI 可以通过其 getter/setter 检测状态变量是否被读取或写入。

  3. 这是假设。当 SwiftUI 创建初始视图层次结构时,它会为每个视图执行以下操作:

    • 设置当前视图

    • 运行视图的主体代码(我发现像 VStack 这样的布局类型的主体具有 Never 类型。我想它有一种等效的方式来遍历其子视图)。

    • 如果 SwiftUI 检测到视图的状态变量被读取,它会将状态变量和视图之间的依赖关系保存在某处。因此,如果应用程序稍后修改了该状态变量,视图的主体将被执行以更新视图。

      • 场景一:如果视图主体不读取视图的状态变量,则不会触发上述机制。因此,当应用修改状态变量时,视图不会更新。

      • 场景2:如果视图通过绑定将其状态变量传递给其子视图(即状态变量的投影值),也不会触发上述机制,因为读取/写入状态变量的投影值是通过Binding属性包装器的 getter/setter,而不是 State 属性包装器的 getter/setter。


更新2

一个有趣的细节。请注意第 7 行中的 += 运算符。它通常实现为读取和写入。所以第 7 行的按钮处理程序也读取状态变量。但是,在创建初始视图层次结构时不会调用按钮处理程序,因此根据上面的理论,SwiftUI 不会因为第 7 行而召回ContentView.body。这可以通过删除第 8 行来证明。

更新3

我对原始问题有一个新的、更简单的解释。在我最初的问题中,我提出:

所以 SwiftUI 似乎知道顶层视图的 @State 变量值是否传递给子视图。我想知道它是如何确定的?

这是一个错误的问题。 SwiftUI 很可能并不真正知道状态传递给子视图。相反,它只知道在视图的主体中访问状态(可能使用我上面描述的类似机制)。因此,如果视图的状态发生变化,它会重新评估视图的主体。就是这个简单的规则。

【问题讨论】:

  • 我是这样理解的:当您在 ContentView 的 body 中调用 ctor LabelView(number: counter) 时 - 将绘制 LabelView - 只是因为它是 ContentView 的一部分。每当状态改变时,body 都会被调用。虽然 SwiftUI 中实际上有很多“魔法”,但在这种情况下,我认为没有 :)
  • 嗨@CouchDeveloper,您的解释是我在问题中提到的“常见解释”。但这并不是那么简单。请参阅我在问题中给出的两个示例,即使状态变量发生更改,ContentView.body 也不会执行。
  • 当您删除 LabelView(number: counter) 时,似乎当 not 引用 counter 时,属性包装器 @State 不会在后台创建订阅,因此 body 将不叫。在 WWDC Introducing Swift UI 的某处,声明 @State 变量的一个特殊属性是 SwiftUI 可以观察它们何时被读取和写入。”。这支持了上述假设。访问源代码将揭示魔法。 (如果你只添加 print(counter) 会发生什么?)
  • 添加print(counter) 会导致ContentView.body 被执行(当然)。所以你认为@State 是基于combine 实现的?如果是这样,则需要提前设置订阅,因此应该可以获得状态变量和子视图之间的依赖关系。但是,我怀疑@State 是否真的使用组合来实现。我认为没有必要这样做,因为 swiftui 可以通过 State 属性包装器的 getter 和 setter 轻松观察状态变量的读写。
  • SwiftUI 基于 Combine。 “魔法”发生在属性包装器中。实际上,在引用counter 的那个看似简单的语句中涉及到相当多的代码。不幸的是,Apple 的 Swift UI 不是开源的,但为了弄清楚这可能如何工作,正在尝试重新实现这种机制的和平:gist.github.com/AliSoftware/ecb5dfeaa7884fc0ce96178dfdd326f8

标签: ios swift swiftui


【解决方案1】:

当你使用 state 属性包装器时,SwiftUI 可以知道何时读取了一个变量,因为它可以通过自定义 getter 函数管理访问。 当视图构建器闭包被评估时,SwiftUI 知道它正在评估哪个构建器函数,并可以设置其状态,以便在评估期间读取的任何状态变量都知道是当前正在评估的视图的依赖项。

在运行时:流程可能如下所示:

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

但是有一堆当前视图...

【讨论】:

  • 嗨@Shadowrun IUIC,你的意思是视图构建器建立了状态变量和视图主体闭包之间的依赖关系。在我的示例代码中,第 8 行是状态变量与 LabelView 关联的唯一位置。所以必须在 BuildExpression() 中创建依赖项,对吗?但是从这个文档 (github.com/apple/swift-evolution/blob/…) 来看, buildExpression() 只能转换表达式的返回值。它不能真正解析表达式。那么它怎么知道func的参数是状态变量呢?
  • 不完全是,不是视图构建器本身。 SwiftUI 运行时知道它将通过调用其主体函数来重新计算视图。在它调用该 body 函数之前,它设置状态以说要注意现在开始调用的任何 @state getter 函数,(并且当函数返回时,停止观察状态 getter 以将视图 V 与调用的状态变量相关联)这就是我的想法无论如何
  • 嗯...我不确定你的意思。不过还是谢谢。
  • 感谢以上更新。我想我们都同意它的 State 属性包装器的 getter 可以解决问题。但我怀疑您流程中的细节不正确。根据你的伪代码,当 ContentView 的状态变量 chagnes 时,它的 body 函数应该被执行。这不是真的。您可以运行我上面的示例代码。点击按钮(注意这会改变状态)。你会看到控制台上只打印了“LabelView”。这意味着当状态发生变化时,不会重新执行 ContentView.body()。
  • 看来,当视图生成器处理第 8 行时,您代码中的当前视图应该是 VStack。这样一来,您的理论就有效了:) 就是说,恐怕在我对此非常有信心之前,我还不能接受您的答案。谢谢。
【解决方案2】:

不是答案

我是一个网络人,不熟悉 swift,但看到这篇文章并觉得很有趣。我很想知道这段代码会如何表现?认为这会测试你的理论:

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

回答你的问题:

你知道 ReactJS 是否有类似的概念或对象,如@State、@Binding、@Environment 等?

需要明确的是,ReactJS 中没有隐式的 getter/setter 跟踪,它会自动触发视图更新(也称为重新渲染)。触发重新渲染的唯一 API 是对 setState() 函数的显式调用。

不过,在 React 中也有与 SwiftUI 的 @State@Binding 相当的功能。简而言之,@State对应this.state@Binding对应this.props

一个典型的 React “类组件”如下所示:

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

【讨论】:

  • 哇,上面的示例代码肯定表明您比我更了解 SwiftUI(我知道 Swift,但我才刚刚开始学习 SwiftUI :) 说真的,这是非常聪明的测试。 IUIC,关键设计是初始视图创建和后期视图更新具有不同的代码路径。测试结果是,当点击按钮时,没有输出。我认为这证明了我更新中的假设。发生的情况是: 1) 假设 Bool.random() 返回 True,调用 counter1 的 getter 并添加对它的依赖。 2)但是,点击按钮更新计数器2,因此ContentView.body不会被执行。
  • 顺便说一句,因为你正在做网络编程。我有个问题。 SwiftUI 与 ReactJS 类似,都使用虚拟 DOM(在 SwiftUI 中它是虚拟视图层次结构,而不是虚拟 DOM,但概念相似)。你知道 ReactJS 是否有类似的概念或对象,如@State@Binding@Environment 等?
  • 仅供参考,有一些开源项目旨在为 Web 编程提供与 SwiftUI 兼容的 API,例如 github.com/SwiftWebUI/SwiftWebUIgithub.com/TokamakUI/Tokamak。所以如果你好奇,你可能想查看他们的源代码,看看他们是如何实现视图更新依赖的。我计划在今年晚些时候这样做。
  • 很高兴知道!在 React 中,触发重新渲染的唯一 API 是对 setState(newValue) 函数的显式调用,该调用会将 newValue 与 oldValue 进行比较,以查看是否需要任何更新。所以没有任何 getter/setter 跟踪。
  • 但是,这种跟踪技术在其他框架中使用。像Vue.js 一样使用与 SwiftUI 相同的想法。 svelte 的作用大致相同,但使用的是编译时分析,而不是运行时跟踪。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-22
  • 2021-08-06
  • 2023-03-12
  • 2021-02-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多