※この記事は iOSDC Japan 2026 に寄稿したパンフレット記事です。
はじめに
これまで 2 回にわたり、@Environment(.keyPath) をテーマに記事を書いてきました。
その中で紹介したかったものの、主題との兼ね合いや紙面の都合から見送った話がいくつかあります。本記事では、そうした「いつか書こうと思っていた話」を余録としてまとめました。
本編の補足として、肩の力を抜いて読んでいただければ幸いです。
今回の記事で紹介するコードはすべて執筆当時最新の安定リリースである Xcode 26.5 にて検証しております。また、内容は全てこれまでの記事を読まれた前提としていませんが、まだ読んでいない方は、ぜひいつか下記のリンクから、これまでの記事を読んでいただけると大変嬉しいです。
https://qiita.com/lovee/items/0893d3ed7813e66d8188
https://qiita.com/lovee/items/a7eb15f766ca4da406e9
値を注入しないなら @Entry は不要
@Environment(\.keyPath) で独自の値を取得するには、@EntryやEnvironmentKey の定義が必要だと思われがちです。しかし、必ずしもそうではありません。もし .environment(\.keyPath, value) で値を注入する必要がない場合は、実は以前の記事で紹介した環境変数を拡張して使う方法で、@Entry や EnvironmentKey を定義せずに、@Environment(\.keyPath) で値を取得できます。
たとえば、端末が低電力モードかどうかを ProcessInfo から取得し、EnvironmentValue として参照したいとします。この値を .environment(\.lowPowerModeEnabled, value) で注入せず、常に ProcessInfo.processInfo から取得するのであれば、EnvironmentValues に getter だけを定義する方法でも参照できます。
extension EnvironmentValues {
var lowPowerModeEnabled: Bool {
ProcessInfo.processInfo.isLowPowerModeEnabled
}
}
struct ContentView: View {
@Environment(\.lowPowerModeEnabled) private var lowPowerModeEnabled
var body: some View {
Text(lowPowerModeEnabled ? "低電力モード" : "通常モード")
}
}
@Environment は getter-only の KeyPath も受け取れます。そのため、これだけで \.lowPowerModeEnabled を参照できます。.environment(\.lowPowerModeEnabled, value) で上書きする必要がなければ、@Entry も EnvironmentKey も定義する必要はありません。
なお、Preview やテストだけで値を差し替えたい場合も、そのためだけに @Entry を付与する必要はありません。View の生成時に @Environment へ別の KeyPath を直接渡せば、Writable ではない KeyPath でも Preview 用の値へ差し替えられます。このような Preview での値の差し替え方については、以前「@Environment(\.keyPath.subPath) があるビューのプレビューの仕方」という記事にまとめています。詳しくは、下のURLからぜひ読んでみてください。
https://qiita.com/lovee/items/c84c8547278362925f84
ただし、この方法で取得する値は、@Entry や EnvironmentKey によって Environment に保持されている値ではありません。そのため、低電力モードの設定が変わっても Environment の変更として扱われず、SwiftUI へ変更は通知されません。値の変更だけをきっかけに View の body が再評価されることはなく、onChange でその変更を確実に検知することもできません。
keyPath 連結で無駄な再評価を避ける
値型を Environment に格納するときは、View での参照先や使い方によっては無駄な再評価が発生することがあります1が、実は以前の記事で紹介した keyPath 連結の手法、すなわち subKeyPath を使うことで、その無駄な再評価を避けられる場合があります。
動きを確認するために、二つのプロパティーを持つ値型を Environment に格納します。
struct StructData: Equatable {
var target: Int = 0
var nonTarget: Int = 0
}
extension EnvironmentValues {
@Entry var structData: StructData = .init()
}
比較用に、\.structData で値全体を取得する View と、\.structData.target で実際に使うプロパティーだけを取得する View を用意します。
struct WholeValueSubview: View {
@Environment(\.structData) private var structData
var target: Int { structData.target }
var body: some View {
let _ = print("WholeValueSubview body re-evaluated")
Text("\(target)")
}
}
struct SubKeyPathSubview: View {
@Environment(\.structData.target) private var target
var body: some View {
let _ = print("SubKeyPathSubview body re-evaluated")
Text("\(target)")
}
}
親 View では StructData を Environment に注入し、target と nonTarget を1秒ごとに交互に更新します。
struct ContentView: View {
@State private var structData = StructData()
var body: some View {
VStack {
WholeValueSubview()
SubKeyPathSubview()
}
.environment(\.structData, structData)
.task {
var updatesTarget = false
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
if updatesTarget {
print("update target")
structData.target += 1
} else {
print("update nonTarget")
structData.nonTarget += 1
}
updatesTarget.toggle()
}
}
}
}
実行すると、target の更新時には両方の body が再評価されます。一方、nonTarget の更新時には WholeValueSubview だけが再評価され、subKeyPath を使った SubKeyPathSubview は再評価されません。
Environment に格納した値型の一部しか使わない場合は、必要なプロパティーまで subKeyPath を指定する。これだけで、関係のない変更による View の再評価を避けられます。
-
ただし
@Observed classのような参照型の場合は、subKeyPath を使わなくても、@Observedの仕組みにより、View のbodyで実際使われているプロパティーが変更された時のみ再評価されます。 ↩
