Lernziel
Nach dieser Lektion kannst du mit @Environment Systemwerte wie Farbschema, Schriftgröße oder Sprache auslesen, eigene @Observable-Objekte über .environment(...) an alle Views weitergeben und eigene Umgebungswerte mit @Entry definieren.
Das Problem: Durchreichen durch viele Ebenen
Stell dir einen View-Baum vor:
AppRoot
└─ MainView
└─ HabitListView
└─ HabitSection
└─ HabitRow
└─ StreakBadge ← braucht die EinstellungenGanz unten braucht StreakBadge die Benutzereinstellungen, etwa ob Streaks angezeigt werden sollen. Mit dem, was du bisher kennst, müsstest du das Einstellungsobjekt durch jede Ebene reichen: MainView(settings:), HabitListView(settings:), HabitSection(settings:) … Fünf Views bekommen einen Parameter, mit dem sie selbst nichts anfangen, nur um ihn weiterzugeben.
Das nennt man Prop Drilling (wörtlich: Eigenschaften durchbohren), und es macht Code zäh: Jede neue Zwischen-View muss den Parameter kennen, und jeder Umbau zieht Änderungen durch viele Dateien.
Die Lösung: die Umgebung (englisch environment).
Was ist die Umgebung?
Du kennst sie schon aus Lektion 2.2 und 2.19: @Environment(\.scenePhase) und @Environment(\.dismiss).
Die Umgebung ist eine Sammlung von Werten, die SwiftUI automatisch an alle Views darunter weitergibt. Jede View kann sich daraus nehmen, was sie braucht, ohne dass die Views dazwischen etwas davon wissen.
Ein Bild für die Umgebung
Stell dir das WLAN in einem Gebäude vor. Der Router steht im Keller, aber jedes Stockwerk und jeder Raum hat Empfang, ohne dass ein Kabel durch jedes Zimmer gezogen werden muss. Wer Internet braucht, verbindet sich einfach.
Systemwerte auslesen
SwiftUI legt viele nützliche Werte in die Umgebung:
struct InfoView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dynamicTypeSize) private var typeSize
@Environment(\.locale) private var locale
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
List {
LabeledContent("Farbschema", value: colorScheme == .dark ? "Dunkel" : "Hell")
LabeledContent("Schriftgröße", value: "\(typeSize)")
LabeledContent("Sprache", value: locale.identifier)
LabeledContent("Breite", value: sizeClass == .compact ? "kompakt" : "normal")
}
}
}Eine kleine Auswahl der wichtigsten Werte:
| Schlüssel | Inhalt | Typische Verwendung |
|---|---|---|
\.colorScheme | .light oder .dark | Abweichendes Aussehen im Dark Mode |
\.dynamicTypeSize | eingestellte Schriftgröße | Layout bei großer Schrift umstellen |
\.horizontalSizeClass | .compact (iPhone hochkant) oder .regular | Layout für iPad oder Querformat |
\.locale | Sprache und Region | Formatierung |
\.dismiss | Aktion zum Schließen | Sheets, Navigation |
\.openURL | Aktion zum Öffnen von Links | Links im Browser öffnen |
\.scenePhase | aktiv, inaktiv, Hintergrund | Speichern beim Verlassen |
\.isEnabled | ob die View aktiv ist | Eigene Elemente ausgrauen |
Die Schreibweise \.colorScheme ist ein Key Path (Lektion 2.16, 🟡-FAQ): ein Verweis auf eine Eigenschaft der Umgebung.
Umgebungswerte setzen
Viele dieser Werte kannst du für einen Teil des View-Baums auch überschreiben:
VStack {
Text("Immer dunkel")
}
.environment(\.colorScheme, .dark)Genau das hast du in Lektion 2.3 mit Previews gemacht: .preferredColorScheme(.dark) und .dynamicTypeSize(...) sind Kurzformen, die Umgebungswerte setzen.
Eigene Objekte in die Umgebung legen
Jetzt das eigentliche Ziel: Deinen eigenen @Observable-Store allen Views zugänglich machen.
Schritt 1: An einer hohen Stelle das Objekt erzeugen und mit .environment(...) in die Umgebung legen:
@main
struct HabitsApp: App {
@State private var settings = AppSettings()
var body: some Scene {
WindowGroup {
MainView()
.environment(settings)
}
}
}Schritt 2: In jeder beliebigen View darunter mit @Environment herausholen:
@Observable
class AppSettings {
var showStreaks = true
var accentColor = Color.blue
}
struct StreakBadge: View {
@Environment(AppSettings.self) private var settings
let streak: Int
var body: some View {
if settings.showStreaks && streak > 0 {
Text("🔥 \(streak)")
.font(.caption)
.foregroundStyle(settings.accentColor)
}
}
}Beachte die Schreibweise: Für eigene Objekte steht in @Environment(...) der Typ mit .self, kein Key Path. SwiftUI sucht in der Umgebung nach einem Objekt genau dieses Typs.
MainView, HabitListView und alle anderen Views dazwischen müssen von AppSettings nichts wissen. Kein Durchreichen mehr.
Erwartetes Ergebnis
StreakBadge zeigt die Streak in der eingestellten Akzentfarbe. Schaltet irgendwo in der App jemand showStreaks aus, verschwinden alle Badges sofort.
Ändern über die Umgebung
Methoden aufrufen und Eigenschaften setzen geht direkt:
Button("Streaks ausblenden") {
settings.showStreaks = false
}Brauchst du Bindings, etwa für einen Toggle, kombinierst du @Environment mit @Bindable lokal in body (Lektion 3.2):
struct SettingsView: View {
@Environment(AppSettings.self) private var settings
var body: some View {
@Bindable var settings = settings
Form {
Toggle("Streaks anzeigen", isOn: $settings.showStreaks)
ColorPicker("Akzentfarbe", selection: $settings.accentColor)
}
}
}Eigene Umgebungswerte mit @Entry
Neben Objekten kannst du auch einfache Werte in die Umgebung legen, etwa einen Abstand, eine Farbe oder einen Modus. Dafür erweiterst du EnvironmentValues:
extension EnvironmentValues {
@Entry var cardCornerRadius: CGFloat = 16
}@Entry ist ein Makro, das einen neuen Umgebungswert mit Standardwert anlegt. Danach funktioniert er wie die eingebauten:
struct Card: View {
@Environment(\.cardCornerRadius) private var radius
var body: some View {
RoundedRectangle(cornerRadius: radius)
.fill(.blue.opacity(0.2))
.frame(height: 100)
}
}
// Überschreiben für einen Teilbaum:
VStack {
Card()
Card()
}
.environment(\.cardCornerRadius, 4)Das ist ideal für Designvorgaben, die für ganze Bereiche gelten sollen. In Lektion 3.29 baust du damit ein kleines Designsystem.
Wann Umgebung, wann Parameter?
Die Umgebung ist bequem, aber kein Allheilmittel:
| Umgebung | Parameter (let, @Binding) |
|---|---|
| Daten, die viele Views in verschiedenen Tiefen brauchen | Daten, die eine bestimmte View braucht |
| App-weite Dinge: Einstellungen, angemeldeter Benutzer, Hauptdatenspeicher | Ein einzelner Datensatz, z. B. das Buch für die Detailansicht |
| Abhängigkeit ist „unsichtbar“ | Abhängigkeit ist im Initializer sichtbar |
Achtung
Der Nachteil der Umgebung: Man sieht einer View von außen nicht an, dass sie etwas aus der Umgebung braucht. Fehlt das Objekt, stürzt die App ab. Verwende die Umgebung deshalb gezielt für wirklich app-weite Dinge und nicht als Abkürzung für jeden Parameter.
Typische Fehler
Objekt fehlt in der Umgebung
Fehlermeldung
Laufzeitfehler: No Observable object of type AppSettings found. A View.environment(_:) for AppSettings may be missing as an ancestor of this view.
Eine View verlangt @Environment(AppSettings.self), aber weiter oben hat niemand .environment(settings) gesetzt. Besonders häufig in Previews und Sheets:
#Preview {
StreakBadge(streak: 5)
.environment(AppSettings())
}Sheets erben die Umgebung der View, an der sie hängen. Liegt das Objekt dort vor, ist es auch im Sheet verfügbar.
Key Path statt Typ
@Environment(\.AppSettings) private var settingsFehlermeldung
Value of type 'EnvironmentValues' has no member 'AppSettings'
Für eigene @Observable-Objekte heißt es @Environment(AppSettings.self). Key Paths gibt es nur für Werte in EnvironmentValues.
$ direkt auf @Environment
Toggle("Streaks", isOn: $settings.showStreaks)Fehlermeldung
Cannot find '$settings' in scope
@Environment liefert kein $. Verwende @Bindable var settings = settings in body.
FAQ
Anfänger
Was bedeutet das überhaupt?Wo lege ich Objekte in die Umgebung, in der App oder in einer View? Für Dinge, die die ganze App betreffen, in der App-Struct. Für Dinge, die nur ein Bereich braucht, etwa ein Tab, an der obersten View dieses Bereichs.
Kann ich mehrere Objekte in die Umgebung legen?
Ja, beliebig viele, solange sie unterschiedliche Typen haben: .environment(settings).environment(store).
Ist @Environment langsamer als ein Parameter?
Nein, nicht spürbar. Und dank @Observable werden nur Views neu gezeichnet, die die geänderte Eigenschaft wirklich lesen.
Fortgeschritten
Warum verhält es sich so?Was passiert, wenn ich dasselbe Objekt an zwei Stellen in die Umgebung lege? Die View bekommt das nächstgelegene Objekt darüber. So kannst du für einen Teilbaum ein anderes Objekt bereitstellen, etwa ein Testobjekt in einer Preview oder einen eigenen Store für einen Bereich.
Kann ein Umgebungsobjekt optional sein?
Ja: @Environment(AppSettings.self) private var settings: AppSettings?. Dann bekommst du nil statt eines Absturzes, wenn es fehlt. Das ist nützlich für wiederverwendbare Komponenten, die auch ohne das Objekt funktionieren sollen.
Profi
Was passiert unter der Haube?Wie fließt die Umgebung durch den View-Baum?
Jede View bekommt von ihrer Eltern-View ein EnvironmentValues, eine Art Wörterbuch mit typsicheren Schlüsseln. Modifier wie .environment(...) erzeugen eine Kopie mit geändertem Wert und geben diese nach unten weiter. Weil EnvironmentValues ein Werttyp mit Copy-on-Write ist, ist das sehr günstig. Eine View, die einen Umgebungswert liest, hängt von genau diesem Schlüssel ab und wird nur neu berechnet, wenn er sich ändert.
Was erzeugt @Entry?
Früher musste man für jeden Umgebungswert einen eigenen Typ schreiben, der das Protokoll EnvironmentKey erfüllt, plus eine berechnete Eigenschaft mit Getter und Setter. @Entry erzeugt diesen ganzen Code aus einer Zeile. In älteren Codebeispielen siehst du noch die lange Form.
Übungen
Übung 1: Dark-Mode-Hinweis
Baue eine View, die mit @Environment(\.colorScheme) einen Text „Gute Nacht 🌙“ im dunklen und „Guten Tag ☀️“ im hellen Modus zeigt. Prüf beide in Previews.
Lösung Übung 1Erst selbst probieren!
struct GreetingView: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Text(colorScheme == .dark ? "Gute Nacht 🌙" : "Guten Tag ☀️")
.font(.largeTitle)
}
}
#Preview("Hell") { GreetingView() }
#Preview("Dunkel") { GreetingView().preferredColorScheme(.dark) }Übung 2: Benutzer in der Umgebung
Baue eine @Observable-Klasse Session mit einem optionalen Benutzernamen. Leg sie in der App-Struct in die Umgebung. Eine tief verschachtelte HeaderView zeigt „Hallo, …!“ oder einen Anmelden-Button. Eine AccountView an anderer Stelle bietet „Abmelden“ an. Beide greifen über @Environment auf dasselbe Objekt zu.
Übung 3: Kompaktmodus
Leg mit @Entry einen Umgebungswert isCompact: Bool = false an. Baue eine Listenzeile, die im Kompaktmodus nur den Titel und sonst Titel, Untertitel und Bild zeigt. Schalte den Modus für die ganze Liste über einen Toggle in der Toolbar um.
Zusammenfassung
- Die Umgebung gibt Werte automatisch an alle Views darunter weiter, ohne Durchreichen.
@Environment(\.schlüssel)liest Systemwerte wiecolorScheme,dynamicTypeSize,dismiss..environment(objekt)legt ein@Observable-Objekt in die Umgebung,@Environment(Typ.self)holt es heraus.- Für Bindings:
@Bindable var x = xlokal inbody. @Entryinextension EnvironmentValueslegt eigene Umgebungswerte an.- Umgebung für app-weite Dinge, Parameter für konkrete Daten. Fehlende Objekte führen zum Absturz.
Spickzettel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dismiss) private var dismiss
// bereitstellen
ContentView().environment(settings)
// verwenden
@Environment(AppSettings.self) private var settings
var body: some View {
@Bindable var settings = settings
Toggle("An", isOn: $settings.flag)
}
// eigener Wert
extension EnvironmentValues {
@Entry var spacing: CGFloat = 16
}
.environment(\.spacing, 8)