SwiftUI: PreferenceKey的reduce方法解密

SwiftUI的PreferenceKey聲明如下:

public protocol PreferenceKey {
  associatedtype Value
  static var defaultValue: Self.Value { get }
  static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}

雖然ValuedefaultValue的性質(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è)NumericPreferenceKeyNumericPreferenceKey.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)NumericPreferenceKeydefaultValue,不會(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è)NumericPreferenceKey13給它們的父類
  • 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)用順序

PreferenceKeyreduce方法包含兩個(gè)參數(shù):當(dāng)前的value,和下一個(gè)要合并的值nextValue。
回到我們的例子:

  1. VStack首先從Text("A")接受到值1.由于之前沒有其他的值被累計(jì),這個(gè)值變成了VStack的當(dāng)前值.
  2. 然后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有從AZText,它們都設(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ì)算VStackNumericPreferenceKey值的時(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ì)算VStackNumericPreferenceKey:

  • 首先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ì)算VStackNumericPreferenceKey:

  • 首先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我們想知道我們的VStackNumericPreferenceKey值是什么,以及它什么時(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)部子視圖VStackpreference key根本不計(jì)算,因?yàn)闆]有人讀取它,因此我們的reduce只被調(diào)用一次,只解析根視圖VStackpreference 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的作用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容