2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIAdvent Calendar 2024

Day 6

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

Last updated at Posted at 2024-12-05

この記事はSwiftUI Advent Calendar 2024への寄稿です。

はじめに

私は以前の記事で、@Environment(\.keyPath) を激推ししていました、その時に紹介した高度の使い方の一つに、KeyPathの連結というテクニックも紹介していました:

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

ここだけ不便かも…?

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

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) で注入するしかないかとみなさん思うではないでしょうか。

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

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

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

#Preview {
    ContentView()
        .environment(\.rect, someCGRectValue) // `\.rect` を注入できます
}
NG1
extension EnvironmentValues {
    var rect: CGRect { .zero } // `EnvironmentValues` に対してゲッターしかない Computed Property を持たせた場合
}

#Preview {
    ContentView()
        .environment(\.rect, someCGRectValue) // `\.rect` を注入できません
}
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

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

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

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

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

スクリーンショット 2024-12-04 1.34.16.png

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

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

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

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

TIPS

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

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

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

PreviewValues.swift
struct PreviewValues {
    var cgFloat0: CGFloat { 0 }
    var cgFloat0_5: CGFloat { 0.5 }
    var cgFloat100: CGFloat { 100 }
}
extension EnvironmentValues {
    var preview: PreviewValues { .init() }
}
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ライフを送ってください!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?