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
について詳しく解説しました。
重要ポイントの復習
- 基本機能
- アプリ全体やビュー階層で共有される環境値にアクセス
- システムが提供する多数の環境値が利用可能
- カスタム環境値も定義できる
- よく使う環境値
colorScheme
: ダークモード対応dismiss
: 画面を閉じるopenURL
: URLを開くhorizontalSizeClass
: レスポンシブデザインdynamicTypeSize
: アクセシビリティ対応
- @EnvironmentObjectとの違い
@Environment
: 値型の共有@EnvironmentObject
: オブジェクトの共有
- 実践的な使い方
- ダークモード対応
- レスポンシブレイアウト
- モーダルの閉じる処理
- アクセシビリティ対応
開発のコツ
- システム環境値を積極的に活用する
- カスタム環境値は適切な場面で使う
@Environment
と@EnvironmentObject
を使い分ける- アクセシビリティ環境値を考慮する
@Environment
を使いこなすことで、より柔軟で保守性の高いSwiftUIアプリを開発できるようになります。まずはcolorScheme
やdismiss
などの基本的な環境値から始めて、徐々に他の環境値も使ってみましょう!