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:)却不能这样说,在本文中,让我们深入了解这个神秘的方法.
官方定义
以下是当前swiftUI的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)
这个定义为reduce的核心功能奠定了一些基础,它用于计算视图preference key首选项键值,仅当多个子节点修改该键时才使用。
NumericPreferenceKey
下面是一个简单的preference定义,它的值为整数:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) { ... }
}
从现在开始,任何视图层次结构中的每个视图都为NumericPreferenceKey默认值为0,无论reduce实现如何。
何时调用reduce
想象一个小的视图层次结构,有一个根,两个叶子,中间没有任何东西:
VStack {
Text("A")
Text("B")
}
为清楚起见:
VStack是根,而两个Text是叶。
我们将在不同的场景中使用这个层次结构。
没有更改/设置preference key的子选项
VStack {
Text("A")
Text("B")
}
这里没有视图设置自己的NumericPreferenceKey值,因此,所有视图都有一个NumericPreferenceKey值NumericPreferenceKey.defaultvalue,根据我们的定义,该值为0。
NumericPreferenceKey.reduce将永远不会在文本上调用,因为没有人可以将值传递给Text。
reduce也不会在VStack上回调,因为它的子对象没有设置/传递NumericPreferenceKey值给它们的父对象.
一个子选项更改/设置preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
}
在这种情况下:
-
Text("A")将其NumericPreferenceKey值设置为1,并将其传递给其父选项 -
Text("B")默认NumericPreferenceKey为defaultValue,不会传递任何信息给它的父对象
VStack呢?让我们再次看一下reduce定义:Combines a sequence of values by modifying the previously-accumulated value with the result of a closure that provides the next value.
因为只有设置/更改NumericPreferenceKey值的子选项才会把它传递给他们的父选项,所以VStack只会积累一个值:Text("A")中的1。
因此,再一次使用NumericPreferenceKey.reduce也不会在VStack上调用,并且与VStack关联的NumericPreferenceKey值现在是1。
多个子选项更改/设置preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
.preference(key: NumericPreferenceKey.self, value: 3)
}
在这个例子中:
- 这两个
Text分别设置和传递一个NumericPreferenceKey值1和3给它们的父类 -
VStack累加两个NumericPreferenceKey值之和
SwiftUI不知道要给VStack分配哪个NumericPreferenceKey值,因为它的子节点提供了多个值,这就是我们的NumericPreferenceKey.reduce可以帮助SwiftUI将这些多个值减少为一个,然后将其分配给我们的VStack。
即使传入的所有值都相同,NumericPreferenceKey.reduce也会被调用。
那么VStack的值是多少呢?在回答这个问题之前,我们需要知道传递给VStack的值的顺序。
Reduce调用顺序
PreferenceKey的reduce方法包含两个参数:当前的value,和下一个要合并的值nextValue。
回到我们的例子:
-
VStack首先从Text("A")接受到值1.由于之前没有其他的值被累计,这个值变成了VStack的当前值. - 然后
VStack首先从Text("B")接受到值3,现在SwiftUI需要将这个值与当前值结合起来,因此调用NumericPreferenceKey.reduce使用1作为value参数,3作为nextValue.
这就是SwiftUI头文件中所说以视图树顺序接收其值的含义,reduce方法是一直回调通过声明顺序遍历我们的子视图从第一个到最后一个。
如果VStack有从A到Z的Text,它们都设置了NumericPreferenceKey的值,reduce将首先使用从Text("A")和Text("B")继承来的当前值调用,然后使用新的当前值和Text("C"),等等。
reduce只在兄弟视图之间调用累积它们的值,如果一个VStack子节点有它自己的子节点,同样的概念将被递归应用,然后这个子节点将把它的最终值传递给VStack,而不管它是如何获得的。
最后是计算VStack的NumericPreferenceKey值的时候了,为此,我们需要看一下NumericPreferenceKey.reduce的方法实现。
常见的reduce实现
每个首选项键(preference key)声明都有自己的reduce实现,在这一节中,让我们介绍一些最常见的问题。
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:
- 首先
VStack接受Text("A")传递的值,因为之前没有积累的值,所以这个值将作为VStack当前值的新值 - 然后
VStack接受Text("B")传递的值,现在有两个值reduce是被回调,VStack的新值将是新的建议值(这就是value = nextValue()所做的)。
换句话说,通过这个实现,当多个子对象传递一个值时,reduce将丢弃所有子对象,但最后一个将成为我们视图的值。
reduce空的实现
一个空的reduce实现:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
}
}
让我们再次回到我们的例子,计算VStack的NumericPreferenceKey:
- 首先
VStack接受Text("A")传递的值,因为之前没有积累的值,所以这个值将作为VStack当前值的新值 - 然后
VStack接受Text("B")传递的值,现在有两个值reduce是被回调,但是什么都没发生,因为我们的reduce什么都没做。VStack保持当前值
这个实现与前面的实现相反:我们的视图将保留第一个收集的值,并忽略其余的。
value += nextValue()
其他常见的实现使用reduce将所有值与一些数学运算符(如sum)组合在一起:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value += nextValue()
}
}
在这种情况下,我们的视图的值将是其子视图传递的所有值的总和,即累加操作。
更多的操作
其他值得提及的实现是是数组或字典的操作,reduce方法用于将所有子值分组在一起(通过append(contentsOf:)或类似的方法)。
一旦我们理解了preference key的内部工作原理,就可以直观地阅读和理解reduce的效果。
PreferenceKey是当前状态的方法
与SwiftUI视图一样,preference key值是当前状态的结果,不会持久存在。
例如,如果我们查看value += nextValue() reduce的实现,当前视图值就是当前传递值的总和。
如果其中一个子节点更改了传递的值,SwiftUI将从头开始重新计算视图的preference key值。
对于任何preference key值都是如此,即使是在数组或字典的情况下。
何时触发计算preference key?
如果我们应用中的完整视图是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会忽略它。
我们所有的key实际上都在那里,并在视图层次结构中正确的位置出现,它们只是没有被使用,因此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的 NumericPreferenceKey值是什么,以及它什么时候发生变化,这是我们看到reduce方法被调用所需要的全部内容。
为什么reduce的nextValue是一个函数
当阅读PreferenceKey的定义时,可能会出现一些令人困惑的事情,那就是为什么reduce参数是一个值和一个函数,我们把两个值结合起来,对吧?为什么SwiftUI不能直接给出下一个明确的值呢?
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
原来又是swiftUI懒惰的原因。
让我们以前面的reduce empty实现为例,在一个稍微复杂一些的示例中使用它:
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包含两个子视图,一个Text("A")和一个VStack,这个VStack子视图又有两个Text子视图。
所有的Text在试图中都设置了它们自己的NumericPreferenceKey,我们在根视图调用onPreferenceChange(_:perform:)方法。
让我们计算NumericPreferenceKey的值:
- 首先
VStack接收Text("A")传递的值,因为之前没有积累的值,所以这个值将作为VStack当前值的新值 - 然后
VStack从另一个子视图VStack接收到另一个值,我们的reduce方法被调用
在这个例子中reduce没有做任何事情,我们不需要知道内部子视图VStack传递的确切值是什么。
由于我们不访问nextValue, SwiftUI甚至不会计算它。
这意味着内部子视图VStack的preference key根本不计算,因为没有人读取它,因此我们的reduce只被调用一次,只解析根视图VStack的preference key。
这就是为什么reduce接受一个值和一个方法:nextValue()方法是SwiftUI检查是否确实需要该值的一种方法,如果不需要,则不会解析它。
SwiftUI需要尽可能快速和高效地解析整个视图层次结构,这是一种优化。
结论
SwiftUI的PreferenceKey是一种不太流行的幕后工具,但要实现某种效果,却又不可或缺:
在这篇文章中,我们探索了PreferenceKey的内部工作原理,并揭示了它的reduce方法是如何使用的以及它的用途,从而发现了更多的SwiftUI的作用。