MENU

【SwiftUI入門】@Environmentとは?使い方から@EnvironmentObjectとの違いまで徹底解説

SwiftUIでアプリ開発を行う際、画面全体で共有したい設定や状態を扱う方法として@Environmentがあります。この記事では、@Environmentの基本から実践的な使い方まで、初心者にもわかりやすく解説します。

目次

@Environmentとは?

@Environmentは、SwiftUIのプロパティラッパーの一つで、アプリ全体やビュー階層で共有される環境値(Environment Values)にアクセスするための機能です。

環境値とは?

環境値とは、ビューの階層全体で共有される設定や状態のことです。例えば:

  • ダークモードかライトモードか
  • 画面のサイズ(iPhoneかiPadか)
  • 言語設定
  • アクセシビリティ設定

これらの値は、個々のビューで管理するのではなく、アプリ全体で共有されています。

基本的な構文

@Environment(\.環境値のキー) var 変数名

この構文で、システムが管理している環境値に簡単にアクセスできます。

@Environmentの基本的な使い方

例1:カラースキーム(ダークモード)の取得

最も基本的な使い方として、現在のカラースキームを取得してみましょう。

import SwiftUI

struct ContentView: View {
    // カラースキームにアクセス
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        VStack(spacing: 20) {
            if colorScheme == .dark {
                Text("現在:ダークモード")
                    .foregroundColor(.white)
            } else {
                Text("現在:ライトモード")
                    .foregroundColor(.black)
            }
            
            Text("設定アプリで外観モードを切り替えてみてください")
                .font(.caption)
                .foregroundColor(.gray)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(colorScheme == .dark ? Color.black : Color.white)
    }
}

この例では、ユーザーの端末設定に応じて自動的にダークモードとライトモードが切り替わります。

例2:画面を閉じる(dismiss)

iOS 15以降では、dismiss環境値を使って画面を閉じることができます。

struct DetailView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationView {
            VStack {
                Text("詳細画面")
                    .font(.title)
                
                Button("この画面を閉じる") {
                    dismiss()  // 画面を閉じる
                }
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("詳細")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("キャンセル") {
                        dismiss()
                    }
                }
            }
        }
    }
}

// 使用例
struct MainView: View {
    @State private var showDetail = false
    
    var body: some View {
        Button("詳細を表示") {
            showDetail = true
        }
        .sheet(isPresented: $showDetail) {
            DetailView()
        }
    }
}

従来は@Binding@Environment(\.presentationMode)を使う必要がありましたが、dismissを使うことでよりシンプルに書けます。

よく使う環境値一覧

SwiftUIには、すぐに使える多くの環境値が用意されています。

1. colorScheme – カラースキーム

ライトモードかダークモードかを判定します。

struct ThemeAwareView: View {
    @Environment(\.colorScheme) var colorScheme
    
    var backgroundColor: Color {
        colorScheme == .dark ? .black : .white
    }
    
    var textColor: Color {
        colorScheme == .dark ? .white : .black
    }
    
    var body: some View {
        Text("テーマに応じた色")
            .foregroundColor(textColor)
            .padding()
            .background(backgroundColor)
            .cornerRadius(10)
    }
}

2. horizontalSizeClass – 画面サイズクラス

iPhoneとiPadで異なるレイアウトを表示する際に便利です。

struct ResponsiveView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        if horizontalSizeClass == .compact {
            // iPhoneや縦向きiPad用のレイアウト
            VStack {
                Text("コンパクトなレイアウト")
                Text("(iPhoneサイズ)")
            }
        } else {
            // 横向きiPadなど広い画面用のレイアウト
            HStack {
                Text("広いレイアウト")
                Spacer()
                Text("(iPadサイズ)")
            }
            .padding()
        }
    }
}

3. openURL – URLを開く

外部リンクやアプリ内ブラウザを開く際に使います。

struct LinkView: View {
    @Environment(\.openURL) var openURL
    
    var body: some View {
        VStack(spacing: 16) {
            Button("Appleのサイトを開く") {
                if let url = URL(string: "https://www.apple.com") {
                    openURL(url)
                }
            }
            .buttonStyle(.borderedProminent)
            
            Button("Twitterアプリを開く") {
                // Twitterアプリが入っていればアプリで、なければブラウザで開く
                if let url = URL(string: "twitter://user?screen_name=apple") {
                    openURL(url)
                }
            }
            .buttonStyle(.bordered)
        }
    }
}

4. dynamicTypeSize – テキストサイズ

アクセシビリティ設定のテキストサイズに対応します。

struct AccessibleTextView: View {
    @Environment(\.dynamicTypeSize) var typeSize
    
    var body: some View {
        VStack(spacing: 20) {
            Text("テキストサイズ: \(typeSize.debugDescription)")
                .font(.caption)
            
            Text("このテキストはユーザーの設定に応じて拡大縮小されます")
                .font(.body)
            
            // テキストサイズに応じてアイコンサイズも変更
            Image(systemName: "star.fill")
                .font(.system(size: typeSize.isAccessibilitySize ? 50 : 30))
        }
        .padding()
    }
}

5. locale – ロケール(言語・地域設定)

struct LocaleView: View {
    @Environment(\.locale) var locale
    
    var body: some View {
        VStack(spacing: 12) {
            Text("現在のロケール: \(locale.identifier)")
            Text("言語コード: \(locale.language.languageCode?.identifier ?? "不明")")
            
            // ロケールに応じた日付表示
            Text(Date(), style: .date)
        }
    }
}

実践例:ダークモード対応アプリ

実際のアプリでダークモードに対応する例を見てみましょう。

struct ProfileView: View {
    @Environment(\.colorScheme) var colorScheme
    
    // ダークモードとライトモードで色を切り替える
    private var cardBackground: Color {
        colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.95)
    }
    
    private var primaryText: Color {
        colorScheme == .dark ? .white : .black
    }
    
    private var secondaryText: Color {
        colorScheme == .dark ? Color(white: 0.7) : Color(white: 0.3)
    }
    
    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                // プロフィールカード
                VStack(alignment: .leading, spacing: 12) {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .font(.system(size: 60))
                            .foregroundColor(.blue)
                        
                        VStack(alignment: .leading) {
                            Text("山田太郎")
                                .font(.title2)
                                .foregroundColor(primaryText)
                            Text("iOS Developer")
                                .font(.subheadline)
                                .foregroundColor(secondaryText)
                        }
                    }
                    
                    Divider()
                    
                    Text("SwiftUIを使ってアプリを開発しています。")
                        .foregroundColor(secondaryText)
                }
                .padding()
                .background(cardBackground)
                .cornerRadius(12)
                
                // 統計情報
                HStack(spacing: 16) {
                    StatCard(title: "投稿", value: "42", colorScheme: colorScheme)
                    StatCard(title: "フォロワー", value: "328", colorScheme: colorScheme)
                    StatCard(title: "フォロー中", value: "156", colorScheme: colorScheme)
                }
            }
            .padding()
        }
    }
}

struct StatCard: View {
    let title: String
    let value: String
    let colorScheme: ColorScheme
    
    private var background: Color {
        colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.95)
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Text(value)
                .font(.title)
                .bold()
            Text(title)
                .font(.caption)
                .foregroundColor(.gray)
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(background)
        .cornerRadius(10)
    }
}

モーダルやシートを閉じる実装パターン

dismiss環境値を使った実践的なパターンをいくつか紹介します。

パターン1:フォーム入力画面

struct AddItemView: View {
    @Environment(\.dismiss) var dismiss
    @State private var itemName = ""
    @State private var itemPrice = ""
    @State private var showAlert = false
    
    var body: some View {
        NavigationView {
            Form {
                Section("商品情報") {
                    TextField("商品名", text: $itemName)
                    TextField("価格", text: $itemPrice)
                        .keyboardType(.numberPad)
                }
            }
            .navigationTitle("商品を追加")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("キャンセル") {
                        dismiss()
                    }
                }
                
                ToolbarItem(placement: .confirmationAction) {
                    Button("保存") {
                        if validateInput() {
                            saveItem()
                            dismiss()
                        } else {
                            showAlert = true
                        }
                    }
                    .disabled(itemName.isEmpty)
                }
            }
            .alert("入力エラー", isPresented: $showAlert) {
                Button("OK") { }
            } message: {
                Text("正しい情報を入力してください")
            }
        }
    }
    
    func validateInput() -> Bool {
        return !itemName.isEmpty && Int(itemPrice) != nil
    }
    
    func saveItem() {
        print("保存: \(itemName) - ¥\(itemPrice)")
        // 実際の保存処理
    }
}

// 使用例
struct ItemListView: View {
    @State private var showAddItem = false
    
    var body: some View {
        NavigationView {
            List {
                Text("商品1")
                Text("商品2")
            }
            .navigationTitle("商品一覧")
            .toolbar {
                Button {
                    showAddItem = true
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showAddItem) {
                AddItemView()
            }
        }
    }
}

パターン2:確認ダイアログ付きの閉じる処理

struct EditView: View {
    @Environment(\.dismiss) var dismiss
    @State private var text = ""
    @State private var hasChanges = false
    @State private var showConfirmation = false
    
    var body: some View {
        NavigationView {
            TextEditor(text: $text)
                .padding()
                .onChange(of: text) { _ in
                    hasChanges = true
                }
                .navigationTitle("編集")
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("閉じる") {
                            if hasChanges {
                                showConfirmation = true
                            } else {
                                dismiss()
                            }
                        }
                    }
                }
                .confirmationDialog(
                    "変更を破棄しますか?",
                    isPresented: $showConfirmation,
                    titleVisibility: .visible
                ) {
                    Button("破棄", role: .destructive) {
                        dismiss()
                    }
                    Button("キャンセル", role: .cancel) { }
                }
        }
    }
}

カスタム環境値の作成

独自の環境値を定義して、アプリ全体で共有することもできます。

ステップ1:EnvironmentKeyを定義

// カスタム環境キーを定義
struct AppThemeKey: EnvironmentKey {
    static let defaultValue: AppTheme = .blue
}

enum AppTheme {
    case blue
    case green
    case purple
    
    var color: Color {
        switch self {
        case .blue: return .blue
        case .green: return .green
        case .purple: return .purple
        }
    }
}

ステップ2:EnvironmentValuesを拡張

extension EnvironmentValues {
    var appTheme: AppTheme {
        get { self[AppThemeKey.self] }
        set { self[AppThemeKey.self] = newValue }
    }
}

ステップ3:使用する

struct ParentView: View {
    @State private var selectedTheme: AppTheme = .blue
    
    var body: some View {
        VStack {
            Picker("テーマ", selection: $selectedTheme) {
                Text("ブルー").tag(AppTheme.blue)
                Text("グリーン").tag(AppTheme.green)
                Text("パープル").tag(AppTheme.purple)
            }
            .pickerStyle(.segmented)
            .padding()
            
            ChildView()
                .environment(\.appTheme, selectedTheme)
        }
    }
}

struct ChildView: View {
    @Environment(\.appTheme) var theme
    
    var body: some View {
        VStack(spacing: 20) {
            Text("選択されたテーマ")
                .font(.headline)
            
            Circle()
                .fill(theme.color)
                .frame(width: 100, height: 100)
            
            Text("このビューは親から渡されたテーマを使用しています")
                .font(.caption)
                .foregroundColor(.gray)
                .multilineTextAlignment(.center)
        }
        .padding()
    }
}

@Environmentと@EnvironmentObjectの違い

SwiftUIには似た名前の@EnvironmentObjectもあります。両者の違いを理解しましょう。

特徴@Environment@EnvironmentObject
主な用途システム値や単純な値カスタムクラスのインスタンス
扱える型値型(構造体、列挙型など)ObservableObjectプロトコルに準拠したクラス
設定方法.environment(\.key, value).environmentObject(object)
デフォルト値必須不要
変更の通知なし@Publishedプロパティの変更を通知

@Environmentの例(値型)

struct ValueExample: View {
    var body: some View {
        ChildView()
            .environment(\.appTheme, .green)  // 値を渡す
    }
}

@EnvironmentObjectの例(参照型)

// ObservableObjectクラスを定義
class UserSettings: ObservableObject {
    @Published var username = "ゲスト"
    @Published var notificationsEnabled = true
}

struct ParentView: View {
    @StateObject private var settings = UserSettings()
    
    var body: some View {
        ChildView()
            .environmentObject(settings)  // オブジェクトを渡す
    }
}

struct ChildView: View {
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {
        VStack {
            Text("ユーザー名: \(settings.username)")
            
            Toggle("通知", isOn: $settings.notificationsEnabled)
        }
    }
}

どちらを使うべきか?

@Environmentを使う場合:

  • システムの設定値にアクセスしたい
  • 単純な値を共有したい
  • カスタム環境値を定義したい

@EnvironmentObjectを使う場合:

  • アプリ全体で状態を共有したい
  • データモデルをビュー階層全体で使いたい
  • 値の変更を自動的に反映させたい

主要な環境値リファレンス

SwiftUIで使用できる主要な環境値をまとめました。

外観・テーマ

@Environment(\.colorScheme) var colorScheme          // ライト/ダークモード
@Environment(\.colorSchemeContrast) var contrast     // 標準/高コントラスト

画面・レイアウト

@Environment(\.horizontalSizeClass) var hSizeClass   // 水平方向のサイズクラス
@Environment(\.verticalSizeClass) var vSizeClass     // 垂直方向のサイズクラス
@Environment(\.layoutDirection) var layoutDirection  // レイアウト方向(LTR/RTL)

ナビゲーション

@Environment(\.dismiss) var dismiss                  // 画面を閉じる(iOS 15+)
@Environment(\.openURL) var openURL                  // URLを開く
@Environment(\.openWindow) var openWindow            // ウィンドウを開く(macOS)

アクセシビリティ

@Environment(\.accessibilityReduceMotion) var reduceMotion        // アニメーション軽減
@Environment(\.accessibilityReduceTransparency) var reduceTransp  // 透明度軽減
@Environment(\.dynamicTypeSize) var typeSize                      // テキストサイズ
@Environment(\.accessibilityDifferentiateWithoutColor) var diffColor  // 色以外での区別

ロケール・時間

@Environment(\.locale) var locale                    // ロケール(言語・地域)
@Environment(\.timeZone) var timeZone               // タイムゾーン
@Environment(\.calendar) var calendar               // カレンダー

データ管理

@Environment(\.managedObjectContext) var context     // Core Dataのコンテキスト

その他

@Environment(\.isEnabled) var isEnabled             // ビューが有効かどうか
@Environment(\.scenePhase) var scenePhase           // アプリのライフサイクル
@Environment(\.refresh) var refresh                 // pull-to-refresh

実践的な使用パターン

パターン1:レスポンシブデザイン

struct ResponsiveLayout: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass
    
    var isCompact: Bool {
        horizontalSizeClass == .compact
    }
    
    var body: some View {
        Group {
            if isCompact {
                // iPhoneや縦向きの場合
                VStack(spacing: 20) {
                    HeaderView()
                    ContentView()
                    FooterView()
                }
            } else {
                // iPadや横向きの場合
                HStack(spacing: 30) {
                    VStack {
                        HeaderView()
                        ContentView()
                    }
                    FooterView()
                }
            }
        }
        .padding()
    }
}

struct HeaderView: View {
    var body: some View {
        Text("ヘッダー")
            .font(.title)
    }
}

struct ContentView: View {
    var body: some View {
        Text("コンテンツ")
    }
}

struct FooterView: View {
    var body: some View {
        Text("フッター")
            .font(.caption)
    }
}

パターン2:アクセシビリティ対応

struct AccessibleButton: View {
    @Environment(\.dynamicTypeSize) var typeSize
    @Environment(\.accessibilityReduceMotion) var reduceMotion
    
    let title: String
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.body)
                .padding(.vertical, typeSize.isAccessibilitySize ? 16 : 12)
                .padding(.horizontal, typeSize.isAccessibilitySize ? 24 : 16)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
        }
        .animation(reduceMotion ? nil : .default, value: typeSize)
    }
}

パターン3:外部リンクの処理

struct SocialLinksView: View {
    @Environment(\.openURL) var openURL
    
    var body: some View {
        VStack(spacing: 16) {
            Text("SNSでフォローしよう")
                .font(.headline)
            
            HStack(spacing: 20) {
                SocialButton(
                    icon: "bird.fill",
                    color: .blue,
                    action: { openURL(URL(string: "https://twitter.com/example")!) }
                )
                
                SocialButton(
                    icon: "camera.fill",
                    color: .pink,
                    action: { openURL(URL(string: "https://instagram.com/example")!) }
                )
                
                SocialButton(
                    icon: "play.fill",
                    color: .red,
                    action: { openURL(URL(string: "https://youtube.com/@example")!) }
                )
            }
        }
        .padding()
    }
}

struct SocialButton: View {
    let icon: String
    let color: Color
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.title2)
                .foregroundColor(.white)
                .frame(width: 50, height: 50)
                .background(color)
                .clipShape(Circle())
        }
    }
}

よくある質問と解決方法

Q1: 環境値が更新されない

A: 環境値は親ビューから子ビューに伝播します。正しく.environment()修飾子を使っているか確認しましょう。

// 正しい例
ParentView()
    .environment(\.customValue, newValue)

Q2: カスタム環境値を作るべき?

A: 以下の場合はカスタム環境値が適しています:

  • ビュー階層全体で共有したい単純な値がある
  • システム設定のような値を扱いたい
  • 値が頻繁に変更されない

複雑なオブジェクトや頻繁に変更される状態は、@EnvironmentObject@StateObjectの使用を検討しましょう。

Q3: @Environment vs @State

A: 使い分けのポイント:

  • @State: 単一ビュー内の状態管理
  • @Environment: システム値やビュー階層全体で共有する値

まとめ:@Environmentをマスターしよう

この記事では、SwiftUIの@Environmentについて詳しく解説しました。

重要ポイントの復習

  1. 基本機能
    • アプリ全体やビュー階層で共有される環境値にアクセス
    • システムが提供する多数の環境値が利用可能
    • カスタム環境値も定義できる
  2. よく使う環境値
    • colorScheme: ダークモード対応
    • dismiss: 画面を閉じる
    • openURL: URLを開く
    • horizontalSizeClass: レスポンシブデザイン
    • dynamicTypeSize: アクセシビリティ対応
  3. @EnvironmentObjectとの違い
    • @Environment: 値型の共有
    • @EnvironmentObject: オブジェクトの共有
  4. 実践的な使い方
    • ダークモード対応
    • レスポンシブレイアウト
    • モーダルの閉じる処理
    • アクセシビリティ対応

開発のコツ

  • システム環境値を積極的に活用する
  • カスタム環境値は適切な場面で使う
  • @Environment@EnvironmentObjectを使い分ける
  • アクセシビリティ環境値を考慮する

@Environmentを使いこなすことで、より柔軟で保守性の高いSwiftUIアプリを開発できるようになります。まずはcolorSchemedismissなどの基本的な環境値から始めて、徐々に他の環境値も使ってみましょう!

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次