# 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:\))<br>
      {% 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()](/ios/swiftui/view/layout/grids/examples/problem-with-.readsize.md).
* [Preferences](broken://pages/-MIqXYu-EqovuJbj2W-M)
* [📦  PreferenceKeys](broken://pages/-MJgcJwAF10Qvdqj0eoM)
* [📦 Anchor](broken://pages/-MJj-bttKH63gRcwmWPW)
* [.badge()](/ios/swiftui/view/view/view-+-.badge.md)
* [Adaptive Layout](/ios/swiftui/view/layout/adaptive-layout.md)
  {% 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="💈 範例" %}

* [💈 對齊欄位](/ios/swiftui/data-flow/preferences/examples/same-width.md) (`MaxWidth = MaxValue<CGFloat>`)
* [💈 MonthView](/ios/master/todo/exercises/monthview.md) (`Frames = AllValues<CGRect>`)
* [💈 CircleText](/ios/master/todo/exercises/circletext.md) (`MaxSide = FirstNonNil<CGFloat>`)
  {% endtab %}

{% tab title="👥  相關 " %}

* [🌀View + pref](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md)  ⭐
* [📦  PreferenceKeys](broken://pages/-MJgcJwAF10Qvdqj0eoM)
* [Anchor Preferences](/ios/swiftui/data-flow/preferences/anchor-preferences.md)
* [Preferences](broken://pages/-MIqXYu-EqovuJbj2W-M)
  {% 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](/ios/swiftui/data-flow/preferences/anchor-preferences.md)

### ⭐️ 取名原則

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

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

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

👉  自製型別：[📦  PreferenceKeys](broken://pages/-MJgcJwAF10Qvdqj0eoM)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lochiwei.gitbook.io/ios/swiftui/data-flow/preferences/preferencekey.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
