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的性質(zhì)和作用都很清楚,但對(duì)于reduce(value: nextValue:)卻不能這樣說,在本文中,讓我們深入了解這個(gè)神秘的方法.
官方定義
以下是當(dāng)前swiftUI的reduce頭文件:
/// Combines a sequence of values by modifying the previously-accumulated
/// value with the result of a closure that provides the next value.
/// 通過修改前面積累的值和提供下一個(gè)值的閉包的結(jié)果來組合一個(gè)值序列。
///
/// This method receives its values in view-tree order. Conceptually, this
/// combines the preference value from one tree with that of its next
/// sibling.
/// 這個(gè)方法以視圖樹順序接收它的值。從概念上講,這將一個(gè)樹的偏好值與它的下一個(gè)兄弟樹的偏好值組
/// 合在一起
///
/// - 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)
這個(gè)定義為reduce的核心功能奠定了一些基礎(chǔ),它用于計(jì)算視圖preference key首選項(xiàng)鍵值,僅當(dāng)多個(gè)子節(jié)點(diǎn)修改該鍵時(shí)才使用。
NumericPreferenceKey
下面是一個(gè)簡(jiǎn)單的preference定義,它的值為整數(shù):
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) { ... }
}
從現(xiàn)在開始,任何視圖層次結(jié)構(gòu)中的每個(gè)視圖都為NumericPreferenceKey默認(rèn)值為0,無論reduce實(shí)現(xiàn)如何。
何時(shí)調(diào)用reduce
想象一個(gè)小的視圖層次結(jié)構(gòu),有一個(gè)根,兩個(gè)葉子,中間沒有任何東西:
VStack {
Text("A")
Text("B")
}
為清楚起見:
VStack是根,而兩個(gè)Text是葉。
我們將在不同的場(chǎng)景中使用這個(gè)層次結(jié)構(gòu)。
沒有更改/設(shè)置preference key的子選項(xiàng)
VStack {
Text("A")
Text("B")
}
這里沒有視圖設(shè)置自己的NumericPreferenceKey值,因此,所有視圖都有一個(gè)NumericPreferenceKey值NumericPreferenceKey.defaultvalue,根據(jù)我們的定義,該值為0。
NumericPreferenceKey.reduce將永遠(yuǎn)不會(huì)在文本上調(diào)用,因?yàn)闆]有人可以將值傳遞給Text。
reduce也不會(huì)在VStack上回調(diào),因?yàn)樗淖訉?duì)象沒有設(shè)置/傳遞NumericPreferenceKey值給它們的父對(duì)象.
一個(gè)子選項(xiàng)更改/設(shè)置preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
}
在這種情況下:
-
Text("A")將其NumericPreferenceKey值設(shè)置為1,并將其傳遞給其父選項(xiàng) -
Text("B")默認(rèn)NumericPreferenceKey為defaultValue,不會(huì)傳遞任何信息給它的父對(duì)象
VStack呢?讓我們?cè)俅慰匆幌?code>reduce定義:Combines a sequence of values by modifying the previously-accumulated value with the result of a closure that provides the next value.
因?yàn)橹挥性O(shè)置/更改NumericPreferenceKey值的子選項(xiàng)才會(huì)把它傳遞給他們的父選項(xiàng),所以VStack只會(huì)積累一個(gè)值:Text("A")中的1。
因此,再一次使用NumericPreferenceKey.reduce也不會(huì)在VStack上調(diào)用,并且與VStack關(guān)聯(lián)的NumericPreferenceKey值現(xiàn)在是1。
多個(gè)子選項(xiàng)更改/設(shè)置preference key
VStack {
Text("A")
.preference(key: NumericPreferenceKey.self, value: 1)
Text("B")
.preference(key: NumericPreferenceKey.self, value: 3)
}
在這個(gè)例子中:
- 這兩個(gè)
Text分別設(shè)置和傳遞一個(gè)NumericPreferenceKey值1和3給它們的父類 -
VStack累加兩個(gè)NumericPreferenceKey值之和
SwiftUI不知道要給VStack分配哪個(gè)NumericPreferenceKey值,因?yàn)樗淖庸?jié)點(diǎn)提供了多個(gè)值,這就是我們的NumericPreferenceKey.reduce可以幫助SwiftUI將這些多個(gè)值減少為一個(gè),然后將其分配給我們的VStack。
即使傳入的所有值都相同,NumericPreferenceKey.reduce也會(huì)被調(diào)用。
那么VStack的值是多少呢?在回答這個(gè)問題之前,我們需要知道傳遞給VStack的值的順序。
Reduce調(diào)用順序
PreferenceKey的reduce方法包含兩個(gè)參數(shù):當(dāng)前的value,和下一個(gè)要合并的值nextValue。
回到我們的例子:
-
VStack首先從Text("A")接受到值1.由于之前沒有其他的值被累計(jì),這個(gè)值變成了VStack的當(dāng)前值. - 然后
VStack首先從Text("B")接受到值3,現(xiàn)在SwiftUI需要將這個(gè)值與當(dāng)前值結(jié)合起來,因此調(diào)用NumericPreferenceKey.reduce使用1作為value參數(shù),3作為nextValue.
這就是SwiftUI頭文件中所說以視圖樹順序接收其值的含義,reduce方法是一直回調(diào)通過聲明順序遍歷我們的子視圖從第一個(gè)到最后一個(gè)。
如果VStack有從A到Z的Text,它們都設(shè)置了NumericPreferenceKey的值,reduce將首先使用從Text("A")和Text("B")繼承來的當(dāng)前值調(diào)用,然后使用新的當(dāng)前值和Text("C"),等等。
reduce只在兄弟視圖之間調(diào)用累積它們的值,如果一個(gè)VStack子節(jié)點(diǎn)有它自己的子節(jié)點(diǎn),同樣的概念將被遞歸應(yīng)用,然后這個(gè)子節(jié)點(diǎn)將把它的最終值傳遞給VStack,而不管它是如何獲得的。
最后是計(jì)算VStack的NumericPreferenceKey值的時(shí)候了,為此,我們需要看一下NumericPreferenceKey.reduce的方法實(shí)現(xiàn)。
常見的reduce實(shí)現(xiàn)
每個(gè)首選項(xiàng)鍵(preference key)聲明都有自己的reduce實(shí)現(xiàn),在這一節(jié)中,讓我們介紹一些最常見的問題。
value = nextValue()
最常見的定義是將nextValue()賦值給value,則NumericPreferenceKey實(shí)現(xiàn)如下:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value = nextValue()
}
}
讓我們回到Text("A")和Text("B")都傳遞一個(gè)值的例子,計(jì)算VStack的NumericPreferenceKey:
- 首先
VStack接受Text("A")傳遞的值,因?yàn)橹皼]有積累的值,所以這個(gè)值將作為VStack當(dāng)前值的新值 - 然后
VStack接受Text("B")傳遞的值,現(xiàn)在有兩個(gè)值reduce是被回調(diào),VStack的新值將是新的建議值(這就是value = nextValue()所做的)。
換句話說,通過這個(gè)實(shí)現(xiàn),當(dāng)多個(gè)子對(duì)象傳遞一個(gè)值時(shí),reduce將丟棄所有子對(duì)象,但最后一個(gè)將成為我們視圖的值。
reduce空的實(shí)現(xiàn)
一個(gè)空的reduce實(shí)現(xiàn):
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
}
}
讓我們?cè)俅位氐轿覀兊睦?,?jì)算VStack的NumericPreferenceKey:
- 首先
VStack接受Text("A")傳遞的值,因?yàn)橹皼]有積累的值,所以這個(gè)值將作為VStack當(dāng)前值的新值 - 然后
VStack接受Text("B")傳遞的值,現(xiàn)在有兩個(gè)值reduce是被回調(diào),但是什么都沒發(fā)生,因?yàn)槲覀兊?code>reduce什么都沒做。VStack保持當(dāng)前值
這個(gè)實(shí)現(xiàn)與前面的實(shí)現(xiàn)相反:我們的視圖將保留第一個(gè)收集的值,并忽略其余的。
value += nextValue()
其他常見的實(shí)現(xiàn)使用reduce將所有值與一些數(shù)學(xué)運(yùn)算符(如sum)組合在一起:
struct NumericPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value += nextValue()
}
}
在這種情況下,我們的視圖的值將是其子視圖傳遞的所有值的總和,即累加操作。
更多的操作
其他值得提及的實(shí)現(xiàn)是是數(shù)組或字典的操作,reduce方法用于將所有子值分組在一起(通過append(contentsOf:)或類似的方法)。
一旦我們理解了preference key的內(nèi)部工作原理,就可以直觀地閱讀和理解reduce的效果。
PreferenceKey是當(dāng)前狀態(tài)的方法
與SwiftUI視圖一樣,preference key值是當(dāng)前狀態(tài)的結(jié)果,不會(huì)持久存在。
例如,如果我們查看value += nextValue() reduce的實(shí)現(xiàn),當(dāng)前視圖值就是當(dāng)前傳遞值的總和。
如果其中一個(gè)子節(jié)點(diǎn)更改了傳遞的值,SwiftUI將從頭開始重新計(jì)算視圖的preference key值。
對(duì)于任何preference key值都是如此,即使是在數(shù)組或字典的情況下。
何時(shí)觸發(fā)計(jì)算preference key?
如果我們應(yīng)用中的完整視圖是VStack的例子,那么reduce實(shí)際上永遠(yuǎn)不會(huì)被調(diào)用:
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有多個(gè)NumericPreferenceKey值傳遞:這篇文章欺騙了我們嗎?
SwiftUI總是盡可能少地向最終用戶展示最終結(jié)果,在這個(gè)例子中,沒有人在讀取或使用preference key,因此SwiftUI會(huì)忽略它。
我們所有的key實(shí)際上都在那里,并在視圖層次結(jié)構(gòu)中正確的位置出現(xiàn),它們只是沒有被使用,因此SwiftUI不會(huì)花任何時(shí)間來解析它們。
如果我們想看到reduce被調(diào)用,我們需要使用NumericPreferenceKey,方法就是在VStack中添加一個(gè)onPreferenceChange(_:perform:)函數(shù):
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值是什么,以及它什么時(shí)候發(fā)生變化,這是我們看到reduce方法被調(diào)用所需要的全部?jī)?nèi)容。
為什么reduce的nextValue是一個(gè)函數(shù)
當(dāng)閱讀PreferenceKey的定義時(shí),可能會(huì)出現(xiàn)一些令人困惑的事情,那就是為什么reduce參數(shù)是一個(gè)值和一個(gè)函數(shù),我們把兩個(gè)值結(jié)合起來,對(duì)吧?為什么SwiftUI不能直接給出下一個(gè)明確的值呢?
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
原來又是swiftUI懶惰的原因。
讓我們以前面的reduce empty實(shí)現(xiàn)為例,在一個(gè)稍微復(fù)雜一些的示例中使用它:
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) {
}
}
在這里我們用一個(gè)VStack作為根視圖,這個(gè)VStack包含兩個(gè)子視圖,一個(gè)Text("A")和一個(gè)VStack,這個(gè)VStack子視圖又有兩個(gè)Text子視圖。
所有的Text在試圖中都設(shè)置了它們自己的NumericPreferenceKey,我們?cè)诟晥D調(diào)用onPreferenceChange(_:perform:)方法。
讓我們計(jì)算NumericPreferenceKey的值:
- 首先
VStack接收Text("A")傳遞的值,因?yàn)橹皼]有積累的值,所以這個(gè)值將作為VStack當(dāng)前值的新值 - 然后
VStack從另一個(gè)子視圖VStack接收到另一個(gè)值,我們的reduce方法被調(diào)用
在這個(gè)例子中reduce沒有做任何事情,我們不需要知道內(nèi)部子視圖VStack傳遞的確切值是什么。
由于我們不訪問nextValue, SwiftUI甚至不會(huì)計(jì)算它。
這意味著內(nèi)部子視圖VStack的preference key根本不計(jì)算,因?yàn)闆]有人讀取它,因此我們的reduce只被調(diào)用一次,只解析根視圖VStack的preference key。
這就是為什么reduce接受一個(gè)值和一個(gè)方法:nextValue()方法是SwiftUI檢查是否確實(shí)需要該值的一種方法,如果不需要,則不會(huì)解析它。
SwiftUI需要盡可能快速和高效地解析整個(gè)視圖層次結(jié)構(gòu),這是一種優(yōu)化。
結(jié)論
SwiftUI的PreferenceKey是一種不太流行的幕后工具,但要實(shí)現(xiàn)某種效果,卻又不可或缺:
在這篇文章中,我們探索了PreferenceKey的內(nèi)部工作原理,并揭示了它的reduce方法是如何使用的以及它的用途,從而發(fā)現(xiàn)了更多的SwiftUI的作用。