# PreferenceKey

{% tabs %}
{% tab title="⭐️ 重點" %}
{% hint style="info" %}
The <mark style="color:red;">**PreferenceKey**</mark> protocol enables a way to send data _<mark style="color:red;">**up**</mark>_ the <mark style="color:orange;">**view hierarchy**</mark>. :point\_right: [SwiftOnTap](https://swiftontap.com/preferencekey)
{% endhint %}

{% hint style="info" %}
當一開始<mark style="color:red;">**佈局**</mark>時 (layout)，就要<mark style="color:red;">**子視件回報**</mark>一些資料給<mark style="color:red;">**母視件**</mark>才能順利完成佈局的話，就可以考慮使用 <mark style="color:purple;">**PreferenceKey**</mark>。
{% endhint %}
{% endtab %}

{% tab title="🔸 定義" %}
```swift
// ------------------------
//     🅿️ PreferenceKey
// ------------------------

public protocol PreferenceKey {
    // value type
    associatedtype Value
    // default value
    static var defaultValue: Self.Value { get }
    // combine values from different children
    static func reduce(
        value    : inout Self.Value, 
        nextValue: () -> Self.Value
    )
} 

// ------------------------
//     View methods
// ------------------------

// 🔸 (child view) set view preference
func preference<K: PreferenceKey>(
    key  : K.Type = K.self, 
    value: K.Value
) -> some View 

// 🔸 .onPreferenceChange()
// (parent view) react to view preference change
func onPreferenceChange<K>(
    _          key: K.Type = K.self, 
    perform action: @escaping (K.Value) -> Void
) -> some View where 
    K       : PreferenceKey, 
    K.Value : Equatable

// 🔸 .overlayPreferenceValue(key:transform:)
func overlayPreferenceValue<Key: PreferenceKey, T: View>(
    _       key: Key.Type = Key.self, 
    _ transform: @escaping (Key.Value) -> T
) -> some View 

// 🔸 .backgroundPreferenceValue(key:transform:)
func backgroundPreferenceValue<Key: PreferenceKey, T: View>(
    _       key: Key.Type = Key.self, 
    _ transform: @escaping (Key.Value) -> T
) -> some View
```
{% endtab %}

{% tab title="📘 手冊" %}
* SwiftOnTap ⟩ [PreferenceKey](https://swiftontap.com/preferencekey)
* [SwiftUI](https://developer.apple.com/documentation/swiftui)  ⟩ &#x20;
  * [State & Data Flow](https://developer.apple.com/documentation/swiftui/state-and-data-flow)  ⟩  [**PreferenceKey**](https://developer.apple.com/documentation/swiftui/preferencekey)
    * .[**reduce**(value:nextValue:)](https://developer.apple.com/documentation/swiftui/preferencekey/reduce\(value:nextvalue:\))
  * [Views & Controls](https://developer.apple.com/documentation/swiftui/views-and-controls)  ⟩  [View](https://developer.apple.com/documentation/swiftui/view)  ⟩  [State](https://developer.apple.com/documentation/swiftui/view-state)  ⟩ &#x20;
    * .[**preference**(key:value:)](https://developer.apple.com/documentation/swiftui/view/preference\(key:value:\))
    * .[**onPreferenceChange**(\_:perform:)](https://developer.apple.com/documentation/swiftui/view/onpreferencechange\(_:perform:\))\

{% endtab %}

{% tab title="📗 參考 " %}
* [x] AppCoda ⟩ [透過 PreferenceKey 對齊視圖](https://www.appcoda.com.tw/swiftui-preferencekey/)&#x20;
* [ ] Ray ⟩ [SwiftUI View Preferences Tutorial for iOS](https://www.raywenderlich.com/26733845-swiftui-view-preferences-tutorial-for-ios)
* [ ] Samwise ⟩ [How to Layout in SwiftUI?](https://samwize.com/2020/03/10/how-to-layout-in-swiftui/)
* [x] FIVE STAR ⟩&#x20;
  * [ ] [How to read a view size in SwiftUI](https://www.fivestars.blog/articles/swiftui-share-layout-information/)
  * [ ] [How SwiftUI's Preference Keys are propagated](https://www.fivestars.blog/articles/preferencekey-reduce/) - about **reduce**().
* [x] The SwiftUI Lab ⟩&#x20;
  * [x] [Inspecting the View Tree – Part 1: PreferenceKey](https://swiftui-lab.com/communicating-with-the-view-tree-part-1/)
  * [ ] [Inspecting the View Tree – Part 2: AnchorPreferences](https://swiftui-lab.com/communicating-with-the-view-tree-part-2/)
  * [ ] [View Extensions for Better Code Readability](https://swiftui-lab.com/view-extensions-for-better-code-readability/)
* [x] Swift with Majid ⟩&#x20;
  * [x] [The magic of view preferences in SwiftUI](https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/)
  * [ ] [Anchor preferences in SwiftUI](https://swiftwithmajid.com/2020/03/18/anchor-preferences-in-swiftui/)
{% endtab %}

{% tab title="👥  相關 " %}
* can cause subtle problems, 👉 [problem-with-.readsize](../../view/layout/grids/examples/problem-with-.readsize "mention").
* [Preferences](broken-reference)
* [📦  PreferenceKeys](broken-reference)
* [📦 Anchor](broken-reference)
* [view-+-.badge](../../view/view/view-+-.badge "mention")
* [adaptive-layout](../../view/layout/adaptive-layout "mention")
{% endtab %}
{% endtabs %}

## 範例 <a href="#examples" id="examples"></a>

{% tabs %}
{% tab title="📦  MaxValue" %}
```swift
// 📦 MaxValue<T: FloatingPoint>
public struct MaxValue<T: FloatingPoint>: PreferenceKey {
    // value type
    public typealias Value = T?
    // default value (nil == not set) 
    public static var defaultValue: Value { nil }
    // ⭐️ choose max value
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        // ⭐️ [T?] --(compactMap)--> [T] --(max)--> T?
        value = [value, nextValue()].compactMap{ $0 }.max()
    }
}
```
{% endtab %}

{% tab title="📦  AllValues" %}
```swift
// 📦 AllValues<T>
public struct AllValues<T>: PreferenceKey {
    // ⭐️ 收集的資料放在 [T] 裡面
    public typealias Value = [T]
    // ⭐️ 初始值：空陣列
    public static var defaultValue: Value { Value() }
    // ⭐️ 加入新資料的方法：[v1, v2, ...] + [vn]
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()   // nextValue() == [vn]
    }
}
```
{% endtab %}

{% tab title="📦  FirstNonNil" %}
```swift
// 📦 FirstNonNil<T>
public struct FirstNonNil<T>: PreferenceKey {
    public typealias Value = T?
    // default value
    public static var defaultValue: Value { nil }
    // combine values from different child views
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value ?? nextValue()   // nil or first non-nil value
    }
}
```
{% endtab %}

{% tab title="💈 範例" %}
* [💈 對齊欄位](examples/same-width) (`MaxWidth = MaxValue<CGFloat>`)
* [💈 MonthView](../../../master/todo/exercises/monthview) (`Frames = AllValues<CGRect>`)
* [💈 CircleText](../../../master/todo/exercises/circletext) (`MaxSide = FirstNonNil<CGFloat>`)
{% endtab %}

{% tab title="👥  相關 " %}
* [🌀View + pref](examples/same-width/view+pref)  ⭐
* [📦  PreferenceKeys](broken-reference)
* [Anchor Preferences](anchor-preferences)
* [Preferences](broken-reference)
{% endtab %}
{% endtabs %}

## 用法 <a href="#howto" id="howto"></a>

設 <mark style="color:purple;">**K**</mark> 為遵循 **PreferenceKey** 的真實型別 (<mark style="color:red;">**concrete type**</mark>)，則使用這個 PreferenceKey 時，通常必須具備以下「三大要素」：

{% hint style="info" %}
* **K** 本身：負責定義如何「**處理子視件回報的值**」
* **母視件**的 **@State** 變數：通常型別與 **K.Value** 相同，負責「**更新子視件的佈局**」。
* **子視件**的 **@Binding** 變數：負責「**承接來自母視件的更新通知**」。
{% endhint %}

有了這些要素後，還要依照固定的模式，才能順利完成佈局，主要是以下的「三步驟」：

{% hint style="info" %}
* 由**子視件**回報值給 **K**：用 child.[**preference**(key:value:)](https://developer.apple.com/documentation/swiftui/view/preference\(key:value:\)) 回報
* 由 **K** 負責處理回報的值：用 K.[**reduce**(value:nextValue)](https://developer.apple.com/documentation/swiftui/preferencekey/reduce\(value:nextvalue:\)) 處理
* 由**母視件**根據回報的值，更新自己的 @State：用 parent.[**onPreferenceChange**(\_:perform:)](https://developer.apple.com/documentation/swiftui/view/onpreferencechange\(_:perform:\)) 處理
{% endhint %}

整個流程可以簡化為：

{% hint style="info" %}
* 子視件回報 ➜ PreferenceKey 處理 ➜ 母視件更新
{% endhint %}

👉  比較：[Anchor Preferences](anchor-preferences)

### ⭐️ 取名原則

**PreferenceKey** 的名字最好是跟**母視件**的 @**State** 變數一致，例如： 如果我們要找的是某個子視件兩邊長中的**最大邊**，這時可以：

* 將**母視件**的 @**State** 變數取名為： `maxSide`
* 而 **PreferenceKey** 就取名為: `MaxSide`

`👉  參見：`[`💈 CircleText`](../../../master/todo/exercises/circletext)

👉  自製型別：[📦  PreferenceKeys](broken-reference)
