Lektion 3.3 · Stufe 3 – Fortgeschritten

@Environment

ca. 15 Minuten Lesezeit

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:

Text
AppRoot
 └─ MainView
     └─ HabitListView
         └─ HabitSection
             └─ HabitRow
                 └─ StreakBadge     ← braucht die Einstellungen

Ganz 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:

Swift
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üsselInhaltTypische Verwendung
\.colorScheme.light oder .darkAbweichendes Aussehen im Dark Mode
\.dynamicTypeSizeeingestellte SchriftgrößeLayout bei großer Schrift umstellen
\.horizontalSizeClass.compact (iPhone hochkant) oder .regularLayout für iPad oder Querformat
\.localeSprache und RegionFormatierung
\.dismissAktion zum SchließenSheets, Navigation
\.openURLAktion zum Öffnen von LinksLinks im Browser öffnen
\.scenePhaseaktiv, inaktiv, HintergrundSpeichern beim Verlassen
\.isEnabledob die View aktiv istEigene 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:

Swift
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:

Swift
@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:

Swift
@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:

Swift
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):

Swift
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:

Swift
extension EnvironmentValues {
    @Entry var cardCornerRadius: CGFloat = 16
}

@Entry ist ein Makro, das einen neuen Umgebungswert mit Standardwert anlegt. Danach funktioniert er wie die eingebauten:

Swift
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:

UmgebungParameter (let, @Binding)
Daten, die viele Views in verschiedenen Tiefen brauchenDaten, die eine bestimmte View braucht
App-weite Dinge: Einstellungen, angemeldeter Benutzer, HauptdatenspeicherEin 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:

Swift
#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

Swift
@Environment(\.AppSettings) private var settings

Fehlermeldung

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

Swift
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

Anfänger

Ü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!
Swift
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) }
Fortgeschritten

Ü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.

Profi

Ü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 wie colorScheme, dynamicTypeSize, dismiss.
  • .environment(objekt) legt ein @Observable-Objekt in die Umgebung, @Environment(Typ.self) holt es heraus.
  • Für Bindings: @Bindable var x = x lokal in body.
  • @Entry in extension EnvironmentValues legt eigene Umgebungswerte an.
  • Umgebung für app-weite Dinge, Parameter für konkrete Daten. Fehlende Objekte führen zum Absturz.

Spickzettel

Swift
@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)

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.