SwiftUI @EnvironmentObject でアプリ全体に状態を共有する
親から子、子から孫へとプロパティを順番に渡していくと、中間のビューが使いもしないデータをただ中継するだけの「バケツリレー」が発生する。ビュー階層が深くなるほどこの問題は深刻になり、コードの可読性と保守性が低下していく。@EnvironmentObject はこのバケツリレーを解消し、任意の階層のビューから共有データに直接アクセスできる仕組みだ。
基本的な使い方
@EnvironmentObject を使うには 3 つのステップが必要になる。まず ObservableObject に準拠したクラスを用意し、次に .environmentObject() モディファイアでビュー階層に注入し、最後に使いたいビューで @EnvironmentObject として受け取る。
class UserSettings: ObservableObject {
@Published var userName = "ゲスト"
@Published var isDarkMode = false
}このクラスをアプリのルートで注入する。
@main
struct MyApp: App {
@StateObject private var settings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}.environmentObject(settings) を付けた時点で、ContentView 以下のすべてのビュー階層から UserSettings にアクセスできるようになる。
子ビューでの受け取り
注入されたオブジェクトは @EnvironmentObject プロパティラッパーで受け取る。型情報をもとに自動的にマッチングされるため、引数として渡す必要がない。
struct ProfileView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
Text("こんにちは、\(settings.userName)さん")
Toggle("ダークモード", isOn: $settings.isDarkMode)
}
}
}ProfileView は親から settings を受け取っていないにもかかわらず、環境から自動的に取得して使えている。中間のビューにはまったく手を加える必要がない。
バケツリレーとの比較
@EnvironmentObject を使わない場合と使う場合で、コードの構造がどう変わるかを比較してみよう。
RootView → TabView → SettingsView → ProfileView と順番にオブジェクトを渡す必要がある。中間の TabView や SettingsView は使わないのに引数を受け取って子に渡す。
RootView で一度注入すれば、ProfileView が直接アクセスできる。中間のビューは一切変更不要。
複数の EnvironmentObject を使う
異なる型の ObservableObject であれば、複数同時に環境へ注入できる。型によって区別されるため、同じ型を 2 つ注入することはできない点に注意が必要だ。
class AuthManager: ObservableObject {
@Published var isLoggedIn = false
}
class CartManager: ObservableObject {
@Published var items: [String] = []
}
struct MyApp: App {
@StateObject private var auth = AuthManager()
@StateObject private var cart = CartManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(auth)
.environmentObject(cart)
}
}
}子ビューではそれぞれ別々に受け取ることができる。
struct CheckoutView: View {
@EnvironmentObject var auth: AuthManager
@EnvironmentObject var cart: CartManager
var body: some View {
if auth.isLoggedIn {
Text("カート内: \(cart.items.count)点")
} else {
Text("ログインしてください")
}
}
}注入し忘れるとクラッシュする
@EnvironmentObject の最大の注意点は、注入を忘れるとランタイムクラッシュが発生することだ。コンパイル時にはエラーが出ないため、テストやプレビューで気づかないまま本番に持ち込んでしまうリスクがある。
.environmentObject() を付け忘れたビュー階層で @EnvironmentObject にアクセスすると、該当する型が見つからず fatalError が発生する。
アプリのルートで確実に注入する設計にし、プレビューでも .environmentObject() を付ける習慣をつける。NavigationLink の遷移先にも環境は引き継がれるため、通常はルートでの注入で十分だ。
プレビューでは以下のように記述する。
#Preview {
ProfileView()
.environmentObject(UserSettings())
}@EnvironmentObject と @ObservedObject の使い分け
どちらも ObservableObject を監視するプロパティラッパーだが、用途が異なる。
// 直接渡す: 1 対 1 の親子関係が明確な場合
struct ParentView: View {
@StateObject private var model = SomeModel()
var body: some View {
ChildView(model: model)
}
}
// 環境経由: 多くのビューで共有する場合
struct ParentView: View {
@StateObject private var model = SomeModel()
var body: some View {
ChildView()
.environmentObject(model)
}
}少数のビューだけが使うデータなら @ObservedObject で直接渡すほうが依存関係が明確になる。一方、認証情報やテーマ設定のようにアプリ全体で参照されるデータは @EnvironmentObject が適している。
iOS 17 以降の代替
iOS 17 では @Observable マクロとともに @Environment プロパティラッパーが拡張され、@EnvironmentObject の役割を @Environment が担えるようになった。新しい API では型安全性が向上し、注入忘れもコンパイル時に検出しやすくなっている。ただし、既存のコードベースや iOS 16 以前のサポートが必要な場合は @EnvironmentObject が引き続き重要な選択肢となる。