-
- 5.1. value = nextValue()
- 5.2. Empty implementation
- 5.3. value += nextValue()
- 5.4. And many more
SwiftUI的PreferenceKey声明如下:
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
虽然很清楚Value和defaultValue是什么和做什么,但对于*reduce(Value: nextValue:)*却不能这样说: 让我们来深入研究一下这个神秘的方法。
1. Official definition
以下是reduce的当前文档:
/// Combines a sequence of values by modifying the previously-accumulated
/// value with the result of a closure that provides the next value.
///
/// This method receives its values in view-tree order. Conceptually, this
/// combines the preference value from one tree with that of its next
/// sibling.
///
/// - Parameters:
/// - value: The value accumulated through previous calls to this method.
/// The implementation should modify this value.
/// - nextValue: A closure that returns the next value in the sequence.
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
让我们举个例子。
2. NumericPreferenceKey
下面是一个简单的preference定义,拥有一个整数作为其值:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) { ... }
}
从现在开始,任何视图层次结构中的每个视图对于NumericPreferenceKey都有一个默认值为0,而不管reduce实现如何。
3. When is reduce invoked
想象一个小的视图层次结构,有一个根,两个叶子,中间没有任何东西:
VStack {
Text("A")
Text("B")
}
为了清晰起见:VStack是根,而两个text是叶。
我们将在不同的场景中使用这个层次结构。
3.1. No child alters/sets the preference key
VStack {
Text("A")
Text("B")
}
由于没有视图设置NumericPreferenceKey值,所以所有视图都有一个NumericPreferenceKey. defaultvalue的NumericPreferenceKey value,按照我们的定义,该值为0。
NumericPreferenceKey.reduce永远不会在text上调用,因为没有人可以将值传递给叶节点。
reduce在VStack上也没有调用,因为它的子节点没有设置/传递NumericPreferenceKey值给父节点。
3.2. One child alters/sets the preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
}
In this case:
- Text("A") sets its NumericPreferenceKey value to 1 and pass it to its parent
- Text("B") defaults NumericPreferenceKey to defaultValue, and not pass anything to its parent
VStack呢? 让我们再来看看reduce定义:通过修改之前积累的值和提供下一个值的闭包的结果来组合一系列值。
因为只有设置/更改了NumericPreferenceKey值的子节点才会将其传递给父节点,所以VStack只会从Text("A")中积累一个值:1。
再一次,VStack上没有调用NumericPreferenceKey.reduce,与VStack关联的NumericPreferenceKey值现在是1。
3.3. Multiple children alter/set the preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
.preference(key: NumericPreferenceKey.self, value: 3)
}
In this example:
- both Texts set and pass to their parent a NumericPreferenceKey value of 1 and 3, respectively
- VStack accumulates two NumericPreferenceKey values
SwiftUI不知道该给VStack分配哪个NumericPreferenceKey值,因为它的子节点会给出多个值:这就是我们的NumericPreferenceKey.reduce起到了拯救作用的时候,帮助SwiftUI将这些多个值合并为一个,然后将其分配给VStack。
即使所有传递的值都是相同的,也会调用NumericPreferenceKey.Reduce。
那么VStack的value是什么呢? 在回答这个问题之前,我们需要知道值以什么顺序传递给VStack。
4. Reduce call order
PreferenceKey的reduce方法总是包含两个参数:当前value和要合并的下一个value。
回到我们的例子:
- VStack first receives the value 1 from Text("A"). As no other value was previously accumulated, this becomes the current value of VStack
- then VStack receives the value 3 from Text("B"), now SwiftUI needs to combine this value with the current value, therefore calling NumericPreferenceKey.reduce with 1 as the value parameter, and 3 as the nextValue
这就是SwiftUI header 所说的This method receives its values in view-tree order. 总是通过按照声明顺序从头到尾遍历视图的子对象来调用:reduce。
如果我们的VStack有从“A”到“Z”的文本,所有设置它们的NumericPreferenceKey值,reduce将首先用当前值调用,继承自Text(“A”)和Text(“B”),然后用新的当前值和Text(“C”),等等。
reduce只在兄弟级中累积的值之间调用:如果VStack子级有自己的子级,则递归应用相同的概念,然后该子级将传递给VStack它的最终值,不管它是如何获得的。
最后是计算VStack的NumericPreferenceKey值的时候了: 要做到这一点,我们需要看看NumericPreferenceKey.reduce实现。
5. Common reduce implementations
每个preference key声明都有自己的reduce实现: 在本节中,让我们介绍一些最常见的方法。
5.1. value = nextValue()
最常见的定义是将nextValue()赋值给value,这也可以是NumericPreferenceKey的实现:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value = nextValue()
}
}
我们回到我们的例子,其中Text("A")和Text("B")都传递一个值,并计算VStack的NumericPreferenceKey:
- first VStack takes in the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
- then VStack gets the value passed by Text("B"), as we have two values reduce is called, and the new VStack value will be whatever the new proposed value is (that's what value = nextValue() does).
换句话说,在这个实现中,当多个子节点传递一个值时,reduce将丢弃除最后一个之外的所有子节点,这将成为我们视图的值。
5.2. Empty implementation
在之前的文章中,我们用一个空的reduce实现定义了各种preference keys:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
}
}
再一次,让我们回到我们的例子,计算VStack的NumericPreferenceKey:
- first VStack takes in the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
- then VStack gets the value passed by Text("B"), as we have two values reduce is called, and nothing happens, as our reduce does nothing. VStack keeps the current value.
这个实现与前一个相反:我们的视图将保留第一个收集的值,而忽略其余的值。
5.3. value += nextValue()
其他常见的实现使用reduce将所有值与一些数学运算符(如sum)组合起来:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value += nextValue()
}
}
现在应该很直观了,在这种情况下,我们的视图的值将是它的子视图传递的所有值的和。
5.4. And many more
其他值得一提的实现包括Value是数组或字典的首选项,以及使用reduce方法将所有子值组合在一起(通过*append(contentsOf:)*或类似方法)。
一旦我们理解了preference key的内部工作原理,阅读和理解reduce的效果就变得很直观了。
6. PreferenceKey is a function of the current state
与SwiftUI视图一样, preference key 值是当前状态的结果,不会持久化。
例如,如果我们看看value += nextValue()的reduce实现,当前视图的值就是当前传递的值的和: 如果一个子节点改变了传递的值,SwiftUI将从头开始重新计算视图preference key值。
对于任何preference key Value也是如此:即使是数组或字典。我们总是从头开始,没有持久化。
7. When is the preference key computed?
如果我们的应用程序中的完整视图是我们的VStack示例,则reduce实际上不会被调用:
struct ContentView: View {
var body: some View {
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
.preference(key: NumericPreferenceKey.self, value: 3)
}
}
}
这是真的,尽管VStack有多个NumericPreferenceKey值传递:这篇文章欺骗了我们吗?
SwiftUI总是尽可能少地将结果呈现给用户。 在这个示例中,没有人读取或使用 preference key;因此,SwiftUI将忽略它。
我们的所有键都在那里,并在视图层次结构中的适当位置出现。它们只是没有被使用。因此,SwiftUI不会花任何时间来解决这些问题。
如果我们想看到reduce被调用,我们需要以某种方式读取NumericPreferenceKey,一种方法是在VStack中添加*onPreferenceChange(_:perform:)*函数:
struct ContentView: View {
var body: some View {
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
.preference(key: NumericPreferenceKey.self, value: 3)
}
.onPreferenceChange(NumericPreferenceKey.self) { value in
print("VStack's NumericPreferenceKey value is now: \(value)")
}
}
}
onPreferenceChange(_:perform:)告诉SwiftUI我们想知道VStack的VStack的NumericPreferenceKey值以及它什么时候改变:这就是我们要看到reduce方法被调用所需要的所有设置。
8. Why is reduce's nextValue a function?
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
当阅读PreferenceKey的定义时,可能会出现一些令人困惑的事情,即reduce的参数是一个值和一个函数: 我们合并了两个值。为什么SwiftUI不给我们两个值?
The answer is SwiftUI's laziness.
让我们以之前的reduce空实现为例,并在一个稍微复杂一点的示例中使用它:
struct ContentView: View {
var body: some View {
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
VStack {
Text("X")
.preference(key: NumericPreferenceKey.self, value: 5)
Text("Y")
.preference(key: NumericPreferenceKey.self, value: 6)
}
}.onPreferenceChange(NumericPreferenceKey.self) { value in
print("VStack's NumericPreferenceKey value is now: \(value)")
}
}
}
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
}
}
这里我们有一个VStack作为我们的根,这个VStack包含两个孩子:一个文本(“a”)和另一个VStack,反过来,有两个文本作为孩子。
视图中的所有文本设置它们的NumericPreferenceKey值,并在根上调用onPreferenceChange(_:perform:)。
让我们计算根NumericPreferenceKeyvalue:
- first VStack receives the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
- then it receives another value from its other child, the inner VStack, and our reduce method gets called
在这个例子中,reduce什么也不做。我们不需要知道内部VStack传递的确切值是什么。
因为我们没有访问nextValue,所以SwiftUI不会计算它。
这意味着根本不会计算内部VStackpreference key,因为没有人读取它,因此我们只调用reduce一次,只解析根VStackpreference key。
这就是为什么reduce需要一个值和一个方法:nextValue()方法是SwiftUI检查该值是否需要的方法,如果不需要,则不会解析它。
SwiftUI需要尽可能快速高效地解决整个视图层次结构,这是另一个优化。
9. Conclusions
SwiftUI的PreferenceKey是其中一个后台工具,虽然不是很流行,但对于获得某些结果来说却是必不可少的:
在本文中,我们探索了PreferenceKey的内部工作原理,并揭示了它的reduce方法是如何使用的以及它的用途,从而发现了更多的SwiftUI效率。