SwiftUIのAnchorの使い方を学ぶ

# モチベーション

[Inspecting the View Tree – Part 2: AnchorPreferences](https://swiftui-lab.com/communicating-with-the-view-tree-part-2/) を元に、
PreferenceKey と PreferenceKey + Anchor の違いを整理する目的で記事を作成した。

オリジナルの記事が簡潔に正確に書かれているので、本記事を読んだ方は是非そちらも訪れてみてください。

また、本記事は `GeometryReader` および `Preference` を知っている前提に説明を端折っているため、理解されていない方はリンクを貼っておきますので以下の記事も参照ください。

- [SwiftUIの肝となるGeometryReaderについて理解を深める](https://qiita.com/masa7351/items/0567969f93cc88d714ac)
- [SwiftUIのPreferenceKeyを使ったアニメーション実装](https://qiita.com/masa7351/items/fa94a1f320fb45496d16)


# Let's start

![anchor2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45426/a6d4355b-2a34-2b35-59b3-827940ac0398.gif)

上、下で `Preference` と `AnchorPreference` を並びて比較していく。

## PreferenceData

```swift
// Preference
private struct MyTextPreferenceData: Equatable {
    let viewIdx: Int
    let rect: CGRect
}
```

```swift
// AnchorPreference
struct MyTextPreferenceDataAnchor {
    let viewIdx: Int
    let bounds: Anchor<CGRect>
}
```

この `Anchor<T>` の `T` には `CGRect` と `CGPoint` を受け取ることが可能である。

## タップされるView ( January, February ...)

BackgroundにViewを追加 + 追加されるViewの中でGeometryReaderを使うことで、対象のViewの座標(CGRect)をPreferenceによって親から参照できるようにしている。CGRectは、カスタム座標(ここでは、`myZstack`)を指定する。
そして、座標取得用のView自体は見える必要がないので、Color.clear 指定で透明にしている。←このテクニックは頻繁に使うので覚えておくと良い。

```swift
// Preference
private struct MonthView: View {
    @Binding var activeMonth: Int
    let label: String
    let idx: Int
    
    var body: some View {
        Text(label)
            .padding(10)
            .background(MyPreferenceViewSetter(idx: idx))
            .onTapGesture { self.activeMonth = self.idx } // @Bindingを通じて親Viewの@Stateの値を書き換えて更新を走らせている。
    }
}

private struct MyPreferenceViewSetter: View {
    let idx: Int
    
    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: MyTextPreferenceKey.self,
                            value: [MyTextPreferenceData(viewIdx: self.idx, rect: geometry.frame(in: .named("myZstack")))])
        }
    }
}
```

続いて、 `AnchorPreference` を見ていく。こちらはすごくスッキリしている。 

Backgroundの中にViewを追加する必要も、 `GeometryReader` 処理も、 カスタム座標(ここでは、`myZstack`)の設定も行っていない。

これらがない代わりに、 `anchorPreference` が追加されている。
この1行を書くことで、CGRectとして親Viewから座標・サイズの取得が可能になる。第2引数を `value: .bounds`と指定することでCGRectの値が取得できる。 

```swift
// AnchorPreference
private struct MonthView: View {
    @Binding var activeMonth: Int
    let label: String
    let idx: Int
    
    var body: some View {
        Text(label)
            .padding(10)
            .anchorPreference(key: MyTextPreferenceKey.self, value: .bounds, transform: { [MyTextPreferenceDataAnchor(viewIdx: self.idx, bounds: $0)] })            
            .onTapGesture { self.activeMonth = self.idx }
    }
}
```

## 枠表示

緑の枠を表示している処理を見ていく。

Preferenceで記述する場合は以下のようにして表示している。

- Body直下にZStackを全体に囲んでいて、その中で、緑枠 と MonthlyViewを配置する
- レイアウトに変化があった場合に呼ばれるこのメソッド(onPreferenceChange)内で、子Viewの座標を rectsプロパティに登録する
- ZStackに `coordinateSpace(name: "myZstack")` を指定しておりこの範囲を独自の座標系として設定する
- 緑の枠のViewはoffset、frameで上記でrectsに指定した値を元に描画する

```swift
// Preference
    var body: some View {
        ZStack(alignment: .topLeading) {
            // 緑の枠
            RoundedRectangle(cornerRadius: 15).stroke(lineWidth: 3.0).foregroundColor(Color.green)
                .frame(width: rects[activeIdx].size.width, height: rects[activeIdx].size.height)
                .offset(x: rects[activeIdx].minX, y: rects[activeIdx].minY)
                .animation(.easeInOut(duration: 1.0))
            
            VStack {
              // MonthView表示エリア
              ....
            }.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
                // 画面初期表示時、画面回転時、画面のレイアウトが変わった時に呼ばれる
                // ここでrectsにMonthView郡の座標・大きさをrectsに登録
                for p in preferences {
                    self.rects[p.viewIdx] = p.rect
                }
            }
        }.coordinateSpace(name: "myZstack")
    }
```
ZStackは 下に書いたものが優先されるので、
枠を優先させたい場合は、MonthView表示エリア の下に移動する。(この例では枠が図形の前後どちらにあっても大差ないと判断してZStackの並び順を意識していない気がしている。)

次に、Anchorを見ていくことにする。

```swift
// AnchorPreference
    var body: some View {
        VStack {
            ...
        }.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
            return GeometryReader { geometry in
                ZStack(alignment: .topLeading) {
                    self.createBorder(geometry, preferences)
                    
                    HStack { Spacer() } // makes the ZStack to expand horizontally
                    VStack { Spacer() } // makes the ZStack to expand vertically
                }.frame(alignment: .topLeading)
            }
        }
    }
```

Body直下にZStackは存在しておらず、 `preference` の代わりに、 `backgroundPreferenceValue` を記述する。 
この `backgroundPreferenceValue` はMonthViewをパラメータとして受け取るIFとなっている。
Preferenceは、自身の座標を取得するのに Backgroundの中で、 GeometryReader を指定して取得していたが、ここでは、 **background**PreferenceValue であり、 この 「**background**」がキーとなると記憶すると良さそうだ。

ちょっと横道に反れるが

```swift
HStack { Spacer() } // makes the ZStack to expand horizontally
VStack { Spacer() } // makes the ZStack to expand vertically
```
上記のように書くことで、はZStackを画面全体(NavigationAreaは含まない)に広げることが出来る。

```swift
    func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceDataAnchor]) -> some View {
        let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
        let bounds = p != nil ? geometry[p!.bounds] : .zero
        return RoundedRectangle(cornerRadius: 15)
                .stroke(lineWidth: 3.0)
                .foregroundColor(Color.green)
                .frame(width: bounds.size.width, height: bounds.size.height)
                .fixedSize()
                .offset(x: bounds.minX, y: bounds.minY)
                .animation(.easeInOut(duration: 1.0))
    }

```

ここでは、`backgroundPreferenceValue` は すべての `MonthView` が引数として渡ってくるので、その中で、選択した図形のindexと一致しないものをフィルターして、一致したものを緑枠を表示するようにしている。

```swift
let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
```

## ソース全体(AnchorReference)

```swift
struct ContentView : View {
    
    @State private var activeIdx: Int = 0
    
    var body: some View {
        VStack {
            Spacer()
            
            HStack {
                MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
                MonthView(activeMonth: $activeIdx, label: "February", idx: 1)
                MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
                MonthView(activeMonth: $activeIdx, label: "April", idx: 3)
            }
            
            Spacer()
            
            HStack {
                MonthView(activeMonth: $activeIdx, label: "May", idx: 4)
                MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
                MonthView(activeMonth: $activeIdx, label: "July", idx: 6)
                MonthView(activeMonth: $activeIdx, label: "August", idx: 7)
            }
            
            Spacer()
            
            HStack {
                MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
                MonthView(activeMonth: $activeIdx, label: "October", idx: 9)
                MonthView(activeMonth: $activeIdx, label: "November", idx: 10)
                MonthView(activeMonth: $activeIdx, label: "December", idx: 11)
            }
            
            Spacer()
        }.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
            return GeometryReader { geometry in
                ZStack(alignment: .topLeading) {
                    self.createBorder(geometry, preferences)
                    
                    HStack { Spacer() } // makes the ZStack to expand horizontally
                    VStack { Spacer() } // makes the ZStack to expand vertically
                }.frame(alignment: .topLeading)
            }
        }
    }
    
    //  CGRect
    func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceDataAnchor]) -> some View {
        let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
        let bounds = p != nil ? geometry[p!.bounds] : .zero
        return RoundedRectangle(cornerRadius: 15)
                .stroke(lineWidth: 3.0)
                .foregroundColor(Color.green)
                .frame(width: bounds.size.width, height: bounds.size.height)
                .fixedSize()
                .offset(x: bounds.minX, y: bounds.minY)
                .animation(.easeInOut(duration: 1.0))
    }
    
}

// CGRect
struct MyTextPreferenceDataAnchor {
    let viewIdx: Int
    let bounds: Anchor<CGRect>
}

private struct MyTextPreferenceKey: PreferenceKey {
    typealias Value = [MyTextPreferenceDataAnchor]
    
    static var defaultValue: [MyTextPreferenceDataAnchor] = []
    
    static func reduce(value: inout [MyTextPreferenceDataAnchor], nextValue: () -> [MyTextPreferenceDataAnchor]) {
        value.append(contentsOf: nextValue())
    }
}

private struct MonthView: View {
    @Binding var activeMonth: Int
    let label: String
    let idx: Int
    
    var body: some View {
        Text(label)
            .padding(10)
            .anchorPreference(key: MyTextPreferenceKey.self, value: .bounds, transform: { [MyTextPreferenceDataAnchor(viewIdx: self.idx, bounds: $0)] })            
            .onTapGesture { self.activeMonth = self.idx }
    }
}
```

# Anchor< CGPoint >の例を見ていく

上記ではAnchor< CGRect > の例であったので、筆者がより複雑な2つのAnchorを使った例を説明するために記載している。
ここでは、スマートなのは1つのAnchorで表現する方法だが見ていくことにする。

```swift
struct MyTextPreferenceData {
    let viewIdx: Int
    var topLeading: Anchor<CGPoint>? = nil
    var bottomTrailing: Anchor<CGPoint>? = nil
}
```

Optionalにしているのは、初期化時に、topLeadingとbottomTrailingを同時に行わないようにしているためである。

```swift
struct MonthView: View {
    @Binding var activeMonth: Int
    let label: String
    let idx: Int
    
    var body: some View {
        Text(label)
            .padding(10)
            // topLeadingをセット
            .anchorPreference(key: MyTextPreferenceKey.self, value: .topLeading, transform: { [MyTextPreferenceData(viewIdx: self.idx, topLeading: $0)] })
            // bottomTrailingに値をセット
            .transformAnchorPreference(key: MyTextPreferenceKey.self, value: .bottomTrailing, transform: { ( value: inout [MyTextPreferenceData], anchor: Anchor<CGPoint>) in
                value[0].bottomTrailing = anchor
            })
            
            .onTapGesture { self.activeMonth = self.idx }
    }
}
```

topLeadingに値を設定

```swift
.anchorPreference(key: MyTextPreferenceKey.self, value: .topLeading, transform: { [MyTextPreferenceData(viewIdx: self.idx, topLeading: $0)] })
```

定義を見ると、

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

無名引数とreturnを省略しなければ以下のように記述できる。

```swift
.anchorPreference(key: MyTextPreferenceKey.self,
      value: .topLeading,
      transform: { anchor in
                    return [MyTextPreferenceDataAnchor(viewIdx: self.idx, topLeading: anchor)]
})
```

続いて、bottomTrailingをセットしている以下を見るが、

ここでは、先の `anchorPreference` の処理で、 MyTextPreferenceDataAnchor で作成したstructに対して、 bottomTrailingを追加登録しているが

```swift
.transformAnchorPreference(key: MyTextPreferenceKey.self, value: .bottomTrailing, transform: { ( value: inout [MyTextPreferenceData], anchor: Anchor<CGPoint>) in
                value[0].bottomTrailing = anchor
            })
```

定義を見ると、以下のようになっており、 inout `値型の参照渡し` により引数に渡したものを変更できるようにしており、
この例では、 `[MyTextPreferenceData]` に対してbottomTrailing に anchorの値を設定している。
補足しておくと、この例では、 `value[0]` としているのは、同時に選択されることはなく、単一のものであるため。

```swift
@inlinable public func transformAnchorPreference<A, K>(key _: K.Type = K.self, value: Anchor<A>.Source, transform: @escaping (inout K.Value, Anchor<A>) -> Void) -> some View where K : PreferenceKey
```

枠を表示している部分はCGRectからCGPointに変わったため多少変わっているがほとんど同じである。

```swift

    func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceDataAnchor]) -> some View {
        let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
        // 下記の4行が異なる
        let aTopLeading = p?.topLeading
        let aBottomTrailing = p?.bottomTrailing
        let topLeading = aTopLeading != nil ? geometry[aTopLeading!] : .zero
        let bottomTrailing = aBottomTrailing != nil ? geometry[aBottomTrailing!] : .zero


        return RoundedRectangle(cornerRadius: 15)
            .stroke(lineWidth: 3.0)
            .foregroundColor(Color.green)
            .frame(width: bottomTrailing.x - topLeading.x, height: bottomTrailing.y - topLeading.y) // ここが変わっている
            .fixedSize()
            .offset(x: topLeading.x, y: topLeading.y) // ここが変わっている
            .animation(.easeInOut(duration: 1.0))
    }
```

# まとめ
Referenceスゴイ！！と思っていたが、Anchor Referenceに置き換えることでより簡潔にコードが書けるようになることを学んだ。
SwiftUI-Labさんはまだ数日分記事があるので引き続き読み込んでいきたい。

# 参考
- [Inspecting the View Tree – Part 2: AnchorPreferences](https://swiftui-lab.com/communicating-with-the-view-tree-part-2/)