> For the complete documentation index, see [llms.txt](https://lochiwei.gitbook.io/ios/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lochiwei.gitbook.io/ios/swiftui/data-flow/preferences/anchor-preferences.md).

# Anchor Preferences

[SwiftUI](/ios/swiftui.md) ⟩ [Data Flow](/ios/swiftui/data-flow.md) ⟩ [View Preferences](/ios/swiftui/data-flow/preferences.md) ⟩

{% tabs %}
{% tab title="⭐️ 重點" %}
{% hint style="info" %}

* An <mark style="color:red;">**anchor**</mark> is a <mark style="color:orange;">**wrapper**</mark>**&#x20;around a&#x20;**<mark style="color:orange;">**value**</mark> (e.g., a point) that can be **resolved** inside the <mark style="color:orange;">**coordinate system**</mark> of a <mark style="color:orange;">**diﬀerent view**</mark> somewhere else in the <mark style="color:red;">**view hierarchy**</mark>.
* .<mark style="color:purple;">**onPreferenceChange**</mark> has the <mark style="color:orange;">**requirement**</mark> that the <mark style="color:orange;">value</mark> conforms to <mark style="color:purple;">**Equatable**</mark>.&#x20;
* <mark style="color:purple;">**Anchor**</mark> does <mark style="color:red;">**not**</mark> conform to <mark style="color:purple;">**Equatable**</mark>, use .<mark style="color:purple;">**overlayPreference**</mark> or .<mark style="color:purple;">**backgroundPreference**</mark> instead.  👉 Thinking in SwiftUI
  {% endhint %}

{% hint style="info" %}

* The main goal of <mark style="color:red;">**anchor preferences**</mark> is to **pass** layout data like <mark style="color:red;">**bounds**</mark>, <mark style="color:red;">**center coordinates**</mark>, etc. to its **parent** view.  👉  [Anchor\<T>](/ios/swiftui/shapes/anchor-less-than-t-greater-than.md)
* When using the [Anchor\<Value>](broken://pages/-MJj-bttKH63gRcwmWPW) as an <mark style="color:red;">**index**</mark> to the [GeometryProxy](/ios/swiftui/view/measure/geometryproxy.md), you get the represented <mark style="color:red;">**CGRect**</mark> or <mark style="color:red;">**CGPoint**</mark> value. And as a plus, you get it already translated to the **coordinate space** of the [GeometryReader](/ios/swiftui/view/measure/geometryreader.md) view.
  {% endhint %}
  {% endtab %}

{% tab title="🔸 定義" %}

```swift
// .anchorPreference(key:value:transform:)
func anchorPreference<A, K: PreferenceKey>(
    key     _: K.Type = K.self, 
    value    : Anchor<A>.Source, 
    transform: @escaping (Anchor<A>) -> K.Value
) -> some View 
```

{% endtab %}

{% tab title="📘 手冊" %}

* SwiftUI  ⟩ &#x20;
  * View Layout & Presentation  ⟩ [OutlineGroup](https://developer.apple.com/documentation/swiftui/outlinegroup)  ⟩ [ViewModifiers](https://developer.apple.com/documentation/swiftui/outlinegroup-view-modifiers)  ⟩ &#x20;
    * [.anchorPreference(key:value:transform:)](https://developer.apple.com/documentation/swiftui/outlinegroup/anchorpreference\(key:value:transform:\))
  * [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;
    * .[overlayPreferenceValue(\_:*\_*:)](https://developer.apple.com/documentation/swiftui/view/overlaypreferencevalue\(_:_:\))
    * .[backgroundPreferenceValue(\_:*\_*:)](https://developer.apple.com/documentation/swiftui/view/backgroundpreferencevalue\(_:_:\))
      {% endtab %}

{% tab title="📗 參考" %}

* [ ] Inspecting the View Tree (Part 2): [AnchorPreferences](https://swiftui-lab.com/communicating-with-the-view-tree-part-2/) - The SwiftUI Lab
* [ ] Thinking in SwiftUI, Ch. 5, Custom Layout - Anchors.
  {% endtab %}

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

* use [ GeometryReader](/ios/swiftui/view/measure/geometryreader.md) to solve an [Anchor\<T>](/ios/swiftui/shapes/anchor-less-than-t-greater-than.md) in the coordinate system of another view.
* [📦 GeometryProxy](/ios/swiftui/view/measure/geometryproxy.md)
* [🅿️ PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md)
* [view layout](/ios/swiftui/view/layout.md) ⟩ [Adaptive Layout](/ios/swiftui/view/layout/adaptive-layout.md)
  {% endtab %}

{% tab title="💈 範例" %}

* [💈 MonthView](/ios/master/todo/exercises/monthview.md)
* [selected button with underline](/ios/swiftui/data-flow/preferences/examples/selected-button-with-underline.md) - get bounds of selected button.
  {% endtab %}
  {% endtabs %}

## Examples

{% tabs %}
{% tab title="🔸 ViewPreference" %}

```swift
import SwiftUI

/// 🅿️ HasDefaultValue
public protocol HasDefaultValue {
    static var defaultValue: Self { get }
}

// ------------------------------------------
//     `HasDefaultValue` conforming types
// ------------------------------------------

/// 🌀 CGSize
extension CGSize: HasDefaultValue {
    public static var defaultValue: Self { .zero }
}

/// 🌀 CGRect
extension CGRect: HasDefaultValue {
    public static var defaultValue: Self { .zero }
}

// -----------------------
//     ViewPreference
// -----------------------

/// 🔸 ViewPreference
public enum ViewPreference {
    
    // 將有關 bounds anchor 的資訊放到 view extension methods 的參數中，
    // 盡量讓 method name 不要太長。
    public enum BoundsAnchorType {
        case first
        case last
    }
    
    /// ⭐ ViewPreference type aliases
    public typealias Size = ViewPreference.First<CGSize>
    public typealias FirstBoundsAnchor = ViewPreference.FirstNonNil<Anchor<CGRect>>
    public typealias LastBoundsAnchor  = ViewPreference.LastNonNil<Anchor<CGRect>>
    
    /// 🔸 ViewPreference.First
    public enum First<T: HasDefaultValue>: PreferenceKey {
        public typealias Value = T
        public static var defaultValue: Value { Value.defaultValue }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            // ⭐ ignore all values other than the first
        }
    }
    
    /// 🔸 ViewPreference.FirstNonNil<T>
    public enum FirstNonNil<T>: PreferenceKey {
        public typealias Value = T?
        public static var defaultValue: Value { nil }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value ?? nextValue()   // nil or first non-nil value
        }
    }
    
    /// 🔸 ViewPreference.LastNonNil<T>
    public enum LastNonNil<T>: PreferenceKey {
        public typealias Value = T?
        public static var defaultValue: Value { nil }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            value = nextValue() ?? value   // nil or last non-nil value
        }
    }
}

// -----------------------
//     View extensions
// -----------------------

/// 🌀 CGRect + ViewPreference
extension View {
    
    /// ⭐ usage: `view.actOnSelfSize { size in ... }`
    public func actOnSelfSize(
        action: @escaping (CGSize) -> Void
    ) -> some View
    {
        // 一般的 view 並不知道自己的 size 資訊，但使用：
        // `.background(GeometryReader{ geo in Color.clear })`
        // 就可以在不影響自己的佈局與外觀下，透過 `geo` (GeometryProxy) 物件
        // 得知自己的 size (geo.size)，然後用 .preference() 回報上層。
        self.background( GeometryReader { geo in
            Color.clear
                // ⭐ 下層回報自己的 size
                .preference(key: ViewPreference.Size.self, value: geo.size)
        })
        // ⭐ 上層得知 size，並用 `action` 處理。
        .onPreferenceChange(ViewPreference.Size.self, perform: action)
    }
    
    // ⭐ local abbreviations
    typealias FBA = ViewPreference.FirstBoundsAnchor
    typealias LBA = ViewPreference.LastBoundsAnchor
    
    // ---------------------------------
    // 下層回報資料的「方法命名原則」：
    // 1. 如果只收集其中一個，用：report...()
    // 2. 如果收集很多個，就用：collect...()
    // ----------------------------------
    
    /// ⭐ report bounds anchor (with anchor type)
    @ViewBuilder public func report(
        bounds anchorType: ViewPreference.BoundsAnchorType
    ) -> some View {
        switch anchorType {
        case .first: 
            // ⭐ 回報 anchor preference
            anchorPreference(key: FBA.self, value: .bounds) { $0 }
        //                        ╰─ 1 ──╯         ╰──2──╯  ╰─3──╯
        // 1. 要回報的 anchor preference (K = FirstBoundsAnchor)
        // 2. 要回報的 anchor 種類 (.bounds)
        // 3. 將 Anchor<A> 轉為要回報的資料： @escaping (Anchor<A>) -> K.Value
        //    如果 Anchor<A> 本身就是要回報的資料 (K.Value)，直接使用 `$0` 就可以。
        case .last: 
            anchorPreference(key: LBA.self, value: .bounds) { $0 }
        }
    }
    
    /// ⭐ report bounds anchor
    public func reportBounds() -> some View {
        // ⭐ 回報 anchor preference
        anchorPreference(key: FBA.self, value: .bounds) { $0 }
        //                    ╰─ 1 ──╯         ╰──2──╯  ╰─3──╯
        // 1. 要回報的 anchor preference (K = FirstBoundsAnchor)
        // 2. 要回報的 anchor 種類 (.bounds)
        // 3. 將 Anchor<A> 轉為要回報的資料： @escaping (Anchor<A>) -> K.Value
        //    如果 Anchor<A> 本身就是要回報的資料 (K.Value)，直接使用 `$0` 就可以。
    }
    
    /// ⭐ background by reported bounds (with anchor type)
    @ViewBuilder public func background<V: View>(
        bounds anchorType: ViewPreference.BoundsAnchorType,
        @ViewBuilder content:  @escaping (CGRect) -> V
    ) -> some View
    {
        switch anchorType {
        case .first:
            backgroundPreferenceValue(FBA.self) { anchor in
                GeometryReader { geo in
                    if let anchor = anchor { content(geo[anchor]) }
                }
            }
        case .last:
            backgroundPreferenceValue(LBA.self) { anchor in
                GeometryReader { geo in
                    if let anchor = anchor { content(geo[anchor]) }
                }
            }
        }
    }
    
    /// ⭐ background by reported bounds
    public func backgroundByBounds<V: View>(
        @ViewBuilder content:  @escaping (CGRect) -> V
    ) -> some View
    {
        backgroundPreferenceValue(FBA.self) { anchor in
            GeometryReader { geo in
                if let anchor = anchor { content(geo[anchor]) }
            }
        }
    }
    
    /// ⭐ act on bounds reported
    @ViewBuilder public func actOnBounds(
        _ anchorType: ViewPreference.BoundsAnchorType,
        action: @escaping (Anchor<CGRect>) -> Void
    ) -> some View
    {
        switch anchorType {
        case .first:
            onPreferenceChange(FBA.self) { anchor in
                if let anchor = anchor { action(anchor) }
            }
        case .last:
            onPreferenceChange(LBA.self) { anchor in
                if let anchor = anchor { action(anchor) }
            }
        }
    }
}
```

{% endtab %}

{% tab title="💈範例" %}
{% embed url="<https://youtu.be/9FAMYaMXmww>" %}

```swift
struct ContentView: View {
    
    @State private var isFirst = true
    
    // ⭐ bounds anchor type
    var anchorType: ViewPreference.BoundsAnchorType {
        isFirst ? .first : .last
    }
    
    var body: some View {
        
        // Toggle
        Toggle("First/Last Bounds Anchor", isOn: $isFirst)
            .padding()
            .background(Color.gray)
            
        // ZStack
        ZStack {
            Color.red
            VStack {
                Text("Hello")
                    .padding()
                    .border(Color.black)
                    .report(bounds: anchorType)            // ⭐ report bounds anchor
                Text("Wonderful")
                Text("World")
                    .report(bounds: anchorType)            // ⭐ report bounds anchor
            }.background(bounds: anchorType) { bounds in   // ⭐ background with bounds  
                ZStack(alignment: .leading) {
                    Rectangle()
                        .fill(Color.orange)
                        .frame(
                            width: bounds.width/3, 
                            height: bounds.height
                        )
                        .offset(x: bounds.minX, y: bounds.minY)
                    Rectangle()
                        .fill(Color.purple)
                        .frame(
                            width: bounds.width * 2 / 3, 
                            height: bounds.height)
                        .offset(
                            x: bounds.width/3 + bounds.minX, 
                            y: bounds.minY)
                }
            }.actOnBounds(anchorType){ anchor in              // ⭐ act on reported bounds
                print(anchor)
            }
        }// ZStack
    }// body
}// ContentView
```

{% endtab %}

{% tab title="⭐️ 解說" %}
如果子視件 childView 要公開它的 frame 給別的視件使用，可以用下列的語法來宣告：

{% hint style="info" %}
以下假設 `typealias FrameAnchors = AllValues<Anchor<CGRect>>`\
👉   關於 **AllValues\<T>**，請參閱： [PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md#ding-yi-shi-yong-fan-xing)。
{% endhint %}

```swift
// ⭐️  1. 將自己的 frame (用 value: .bounds 指定) 登記到 FrameAnchors 中
//     2. $0 的類型是 Anchor<CGRect>
//     3. [$0] 是為了配合 FrameAnchors.Value 的類型：[Anchor<CGRect>]
childView.anchorPreference(key: FrameAnchors.self, value: .bounds) { [$0] }
```

當母視件要用這些 FrameAnchors 來做佈局時，可以用：

```swift
parentView
    // ⭐️ 把公開的 FrameAnchors 拿來用 (配合：GeometryProxy)
    .overlayPreferenceValue(FrameAnchors.self) { (anchors) in
        GeometryReader { geo in
            // 用 GeometryProxy 的 subscript 語法：
            //     geo[anchors[i]] 
            // 取出第 i 個 Anchor<CGRect>
            // 在這個座標系統中的 frame (CGRect)，
            // 然後做相關的佈局。
        }
    }
```

這樣做的好處是：我們不需要在 parentView 身上另行定義 CoordinateSpace。
{% endtab %}
{% endtabs %}
