@Environment(\.keyPath.subPath)があるビューのプレビューの仕方

:::note
この記事は[SwiftUI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/swiftui)への寄稿です。
:::

# はじめに

私は[以前の記事](https://qiita.com/lovee/items/0893d3ed7813e66d8188)で、`@Environment(\.keyPath)` を激推ししていました、その時に紹介した高度の使い方の一つに、[KeyPathの連結](https://qiita.com/lovee/items/0893d3ed7813e66d8188#keypathの連結)というテクニックも紹介していました：

```swift:ContentView.swift
struct ContentView: View {
    @Environment(\.rect.midX) var midX: CGFloat
    // ...
}
```

# ここだけ不便かも…？

ところがこの時、人によっては一つだけ不便を感じることもあるでしょう、そう、プレビューの時です。この場合、`.environment(\.rect.midX, cgFloatValue)` で注入できないのです。

```swift:ContentView.swift
#Preview {
    ContentView()
        .environment(\.rect.midX, cgFloatValue) // Error: Cannot convert value of type 'KeyPath<EnvironmentValues, CGFloat>' to expected argument type 'WritableKeyPath<EnvironmentValues, CGFloat>'
}
```

だからこの場合は素直に `.environment(\.rect, cgRectValue)` で注入するしかないかとみなさん思うではないでしょうか。

```swift:ContentView.swift
#Preview {
    ContentView()
        .environment(\.rect, cgRectValue)
```

:::note
正確にいうと、そのまま `.environment`注入できないのは `\.rect.midX` だからではなく、`EnvironmentValues` に対してセッターかないからです。つまり、`\.rect` の場合でも、セッターがあれば問題ないですが、セッターがなかったら `\.rect.midX` と同じく注入できないです：

```swift: OK
extension EnvironmentValues {
    @Entry var rect: CGRect = .zero // ← `EnvironmentValues` に対してセッターを持つ可変の `rect` という Stored Property を持たせた場合
}

#Preview {
    ContentView()
        .environment(\.rect, someCGRectValue) // `\.rect` を注入できます
}
```

```swift:NG1
extension EnvironmentValues {
    var rect: CGRect { .zero } // `EnvironmentValues` に対してゲッターしかない Computed Property を持たせた場合
}

#Preview {
    ContentView()
        .environment(\.rect, someCGRectValue) // `\.rect` を注入できません
}
```

```swift:NG2
extension EnvironmentValues {
    let rect: CGRect = .zero // `EnvironmentValues` に対してそもそもセッターがない定数 Property を持たせた場合も
}

#Preview {
    ContentView()
        .environment(\.rect, someCGRectValue) // `\.rect` を注入できません
}
```
:::

# 改善の試み

まあ `CGRect` くらいならまだそんなに複雑な型ではないし、イニシャライザーもそこまで面倒ではないからなんとかできますが、実際の開発ではより複雑な依存も絶対登場するから、可能ならやはりそのまま `midX` だけ注入したい、みたいな時ありますよね。

ところが、実はもしこの `@Environment(\.rect.midX)` のプロパティーを作る時に `private` とかでさえ隠していなければ、`ContentView` タイピングした時、`midX` という引数を受け取るイニシャライザーも登場しているのお気づきでしょうか

![スクリーンショット 2024-12-04 1.22.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63998/ab5cc729-b441-83c5-39d6-312b84cfd676.png)

そう、見ての通り、実は `.environment` で注入する以外に、直接 `midX` をイニシャライズ時に設定する方法もあります。

ところが、スクショ見る限り、この `midX` は通常の `CGFloat` ではなく、`Environment<CGFloat>` という謎の型を受け取りますね…これはなんでしょう？

実はこの `Environment` は、まさに我々がいつも使っているあの `@Environment` の実態で、そのジェネリクスの `<CGFloat>` はこの `Environment` が出力するものの型です。そう、今回の場合はその `\.rect.midX` の型です

そして試しに `.init` を入力して `Environment` を作ってみると、このようなサジェスチョンが出てきますね：

![スクリーンショット 2024-12-04 1.34.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63998/220dc376-1c3c-149f-cee3-020cd58f887a.png)

なるほど、`EnvironmentValues` から `CGFloat` への `KeyPath` を使ってイニシャライズできるっぽいですね。そうです、実は我々がいつも書いてる `@Environment(\.keyPath)` のこの `\.keyPath` の部分が、まさにここのイニシャライザーに使われている `KeyPath` です。というわけで、勘のいい人ならもう気づいたかもしれません：そう、プレビュー向けの `midX` を作っちゃえばいいです！

```swift:ContentView.swift
private extension EnvironmentValues {
    var midX: CGFloat { 100 }
}

#Preview {
    ContentView(midX: .init(\.midX))
}
```

:::note warn
ただしこのように直接イニシャライザーで `Environment` を生成するのは、実際のプロダクトコードでやってはいけません。公式ドキュメントではそもそも「[Don’t call this initializer directly](https://developer.apple.com/documentation/swiftui/environment/init(_:))」と警告しており、通常はこのような使い方を想定されていません。これはあくまでプロダクトデータに影響せず、ただ `Environment` の内容、すなわち `EnvironmentValues` から取りたいプロパティーをプレビュー向けに書き換えているだけです。
:::

# TIPS

ただ、実際のプレビューでは、たくさんのパターンを確認したいことも多いと思いますので、個人的には愚直な `midX` のような命名はあまりおすすめではなく、代わりに実際に代入したい値をベースに命名する方がいろんなパターンを作りやすいのでおすすめです：

```swift:ContentView.swift
private extension EnvironmentValues {
    var cgFloat0: CGFloat { 0 }
    var cgFloat0_5: CGFloat { 0.5 }
    var cgFloat100: CGFloat { 100 }
}

#Preview {
    ContentView(midX: .init(\.cgFloat0))
    ContentView(midX: .init(\.cgFloat0_5))
    ContentView(midX: .init(\.cgFloat100))
}
```

![スクリーンショット 2024-12-04 1.46.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63998/a299a845-32f9-c41e-ef26-4a166e21940e.png)

更に複数のビューでプレビュー用のデータを共有したい場合は、こんな感じでプレビュー用の型を作るのもアリかもですね、これなら `EnvironmentValues` が汚れる心配もせずに済みます

```swift:PreviewValues.swift
struct PreviewValues {
    var cgFloat0: CGFloat { 0 }
    var cgFloat0_5: CGFloat { 0.5 }
    var cgFloat100: CGFloat { 100 }
}
extension EnvironmentValues {
    var preview: PreviewValues { .init() }
}
```

```swift:ContentView.swift
#Preview {
    ContentView(midX: .init(\.preview.cgFloat0))
    ContentView(midX: .init(\.preview.cgFloat0_5))
    ContentView(midX: .init(\.preview.cgFloat100))
}
```

# あとがき

いかがでしたでしょうか。まあ確かに直接値を入れられず、どうしても一回 `EnvironmentValues` を拡張しないといけないのは多少の手間がかかります。ただこれで場合によっては面倒な複雑なオブジェクトを作るのが免れることも多いので、これで `@Environment(\.keyPath)` の魅力をさらに感じられたら幸いです。これからもぜひこの `@Environment(\.kePath)` を活用して、素敵なSwiftUIライフを送ってください！