[SwiftUI] PreferenceKeyの基本的な仕組みを知る

子ビューから親ビューにデータを渡す仕組みとして頻繁に使用するPreferenceKeyですが、

- reduce()メソッドの実装ってこれで合ってるんだっけ？
- onPreferenceChangeにはどういう値が入ってくる？

といった点について、毎回デバッグして調べてたので、改めて整理しました。

# コード例

ビューとPreferenceKeyを以下の通り実装します。

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle1")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle2")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle3")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle4")
        }
        .onPreferenceChange(StringPreferenceKey.self) { value in
            print("onPreferenceChange() value: \(value)")
        }
    }
}

struct StringPreferenceKey: PreferenceKey {
    typealias Value = String
    static var defaultValue: Value = "Default"

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value = next
    }
}
```

<br>

このビューを画面に表示したとき、コンソールには以下のように出力されます。

```
reduce() value: Rectangle1, nextValue: Rectangle2
reduce() value: Rectangle2, nextValue: Rectangle3
reduce() value: Rectangle3, nextValue: Rectangle4
onPreferenceChange() value: Rectangle4
```

# PreferenceKeyのポイントを整理
ポイントを整理します。

1. ビューに配置されたpreferenceモディファイアは4つ
2. reduce()メソッドは3回呼ばれた
3. reduce()メソッドが3回呼ばれた後、onPreferenceChange()には最後の処理結果が渡される

<br>

reduce()メソッドはビューに配置された同じPreferenceKeyの値をどう処理するかを決める役割を持ちます。
今回は以下のように`value`に`nextValue()`の値を代入する実装にしました（print文を消してわかりやすくしています）。

```swift
static func reduce(value: inout Value, nextValue: () -> Value) {
    value = nextValue()
}
```

<br>

すると、reduce()メソッドが呼ばれるたびに`value`の値は上書きされていき、最後の値である`Rectangle4`がonPreferenceChange()に渡されます。

<br>
<br>

次はPreferenceKeyを以下のように実装してみます。

```swift
struct StringPreferenceKey: PreferenceKey {
    typealias Value = [String]
    static var defaultValue: Value = []

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value.append(contentsOf: next)
    }
}
```

<br>

すると、コンソールには以下のように出力されます。

```
reduce() value: ["Rectangle1"], nextValue: ["Rectangle2"]
reduce() value: ["Rectangle1", "Rectangle2"], nextValue: ["Rectangle3"]
reduce() value: ["Rectangle1", "Rectangle2", "Rectangle3"], nextValue: ["Rectangle4"]
onPreferenceChange() value: ["Rectangle1", "Rectangle2", "Rectangle3", "Rectangle4"]
```

<br>

`value`に値をappendしていくので、最終的には4つの要素を持つ配列がonPreferenceChange()に渡されます。
複数の子ビューのデータを一度に受け取って何か処理したいときはこのように実装すると良さそうです。

# 下層に同じPreferenceKeyが1つしかない場合どうなるのか？
ちなみに、下層に同じPreferenceKeyが1つしかない場合はreduce()メソッドは呼ばれません。

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            Color.clear
                .preference(key: StringPreferenceKey.self, value: ["Rectangle1"])
        }
        .onPreferenceChange(StringPreferenceKey.self) { value in
            print("onPreferenceChange() value: \(value)")
        }
    }
}

struct StringPreferenceKey: PreferenceKey {
    typealias Value = [String]
    static var defaultValue: Value = []

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value.append(contentsOf: next)
    }
}
```

<br>

コンソールの出力。

```
onPreferenceChange() value: ["Rectangle1"]
```

# defaultValueはどこで使われる？
どこで使われるのでしょう...？