【发布时间】: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 变量,这会导致ContentView 和LabelView 都得到更新。一个常见的解释是,因为顶层视图的@State变量发生了变化,所以顶层ivew的body被重新执行了。
但我的实验表明,事情并没有那么简单。
-
例如,如果我在代码中删除了
LabelView(number: counter)调用(参见上面的第 8 行),那么点击按钮将不会执行 ContentView.body()。 -
再举个例子,如果我将
@Binding变量而不是@State变量值传递给子视图,那么点击按钮也不会执行 ContentView.body()。
所以 SwiftUI 似乎知道顶级视图的 @State 变量值是否传递给子视图。我想知道它是如何确定的?我知道编译器有这种信息。但是 SwiftUI 是一个用 Swift 编写的框架。我认为 Swift 没有这种功能。
在写这个问题时,我意识到了一种可能的方法。注意上面的代码是在创建初始视图层次结构时执行的。也就是说,如果将@State 变量传递给子视图,则它的getter 必须在视图更新之前执行。似乎 SwiftUI 可能会利用这一点来确定是否有 @State 变量被传递给子视图(或在顶层视图的主体中访问)。
这就是它的工作方式吗?不知道大家是怎么理解的?谢谢。
更新 1:
以下是我与@Shadowrun 的讨论摘要,他帮助将这些部分组合在一起。我不知道这是否真的发生在幕后,但对我来说似乎是合理的。我把它写下来以防它也帮助别人。
-
SwiftUI 运行时以某种方式维护视图层次结构。例如,
-
对于子视图,这可以在视图构建器的帮助下完成。例如,视图构建器向运行时注册子视图。
-
对于顶级视图,这可以由运行时完成(注意顶级视图的主体是常规计算属性,而不是由视图构建器转换)。
-
-
SwiftUI 可以通过其 getter/setter 检测状态变量是否被读取或写入。
-
这是假设。当 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中调用 ctorLabelView(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