Lektion 2.2 · Stufe 2 – SwiftUI-Einstieg

Anatomie einer App

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kennst du den Aufbau einer SwiftUI-App: was @main, das App-Protokoll, Scene und WindowGroup bedeuten, wie Views ineinander verschachtelt werden, welche Dateien ein neues Projekt enthält und wie du auf den Wechsel zwischen Vorder- und Hintergrund reagierst.

Das Problem: Wo fängt eine App eigentlich an?

In Lektion 0.4 hast du eine App gebaut, indem du ContentView.swift bearbeitet hast. Aber woher weiß das iPhone, dass es beim Start genau diese View anzeigen soll? Und was ist diese zweite Datei, die mit App.swift endet?

Um eine App gezielt aufzubauen, musst du verstehen, wie die Teile zusammenhängen. Zum Glück ist eine SwiftUI-App sehr übersichtlich aufgebaut.

Die drei Ebenen

Jede SwiftUI-App besteht aus drei Ebenen, die ineinander stecken:

Text
App                        ← die ganze Anwendung
 └─ Scene                  ← ein Fenster (auf dem iPhone: der ganze Bildschirm)
     └─ View               ← der Inhalt des Fensters
         ├─ View
         └─ View …         ← beliebig tief verschachtelt

Schauen wir sie uns einzeln an.

Ebene 1: Die App

Leg ein neues Projekt namens Anatomie an und öffne die Datei AnatomieApp.swift:

Swift
import SwiftUI

@main
struct AnatomieApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Das ist der Startpunkt deiner App. Fast alles darin kennst du schon:

  • struct AnatomieApp: eine ganz normale Struct (Lektion 1.12).
  • : App: Sie erfüllt das Protokoll App (Lektion 1.15). Es verlangt eine Eigenschaft body, die eine Scene liefert.
  • var body: some Scene: eine berechnete Eigenschaft mit opakem Typ (Lektion 1.18). Ganz ähnlich wie bei Views, nur dass hier Scene statt View steht.

Neu ist nur @main. Dieses Attribut sagt: „Hier beginnt das Programm.“ Wenn jemand auf das App-Symbol tippt, sucht iOS nach dem Typ mit @main, erzeugt ihn und fragt nach seinem body.

Achtung

In einem Projekt darf es genau einen Typ mit @main geben. Kopierst du etwa eine zweite App-Datei hinein, meldet Xcode einen Fehler.

Ebene 2: Die Scene

Eine Scene ist ein Bereich, den das System für deine App verwaltet, meistens ein Fenster. Auf dem iPhone gibt es praktisch immer nur eins, und es füllt den ganzen Bildschirm.

WindowGroup ist die Scene, die du fast immer verwendest. Sie bedeutet: „Ein Fenster mit diesem Inhalt.“ Auf dem iPad oder Mac kann der Nutzer daraus mehrere Fenster öffnen, auf dem iPhone bleibt es bei einem.

In den geschweiften Klammern steht, welche View das Fenster zeigt: ContentView(). Das ist eine Trailing Closure (Lektion 1.11), genau wie bei VStack.

Tipp

Willst du, dass deine App mit einer anderen View startet, ändere einfach diese eine Zeile: WindowGroup { WelcomeView() }. Der Name ContentView ist nur eine Konvention.

Ebene 3: Die Views

Und dann ContentView.swift:

Swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

ContentView ist die erste View, aber bei Weitem nicht die einzige. In ihrem body stecken weitere Views: ein VStack, darin ein Image und ein Text. Jede davon ist selbst eine View mit eigenem body, auch wenn Apple ihn für dich geschrieben hat.

So entsteht ein Baum aus Views:

Text
ContentView
 └─ VStack (mit .padding)
     ├─ Image("globe")
     └─ Text("Hello, world!")

Jede App ist so aufgebaut, egal wie groß sie ist. Ein großer Bildschirm ist einfach ein tieferer Baum, und ab Lektion 2.21 lernst du, ihn in übersichtliche Teile zu zerlegen.

Eigene Views verschachteln

Du kannst eigene Views genauso verwenden wie Apples Views:

Swift
struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            HeaderView()
            Text("Inhalt der App")
        }
    }
}

struct HeaderView: View {
    var body: some View {
        Text("Meine App")
            .font(.largeTitle)
            .bold()
    }
}

HeaderView() erzeugt eine Instanz deiner Struct, und SwiftUI fügt sie an dieser Stelle in den Baum ein. Mehrere Views dürfen in einer Datei stehen. Üblich ist aber, jeder größeren View eine eigene Datei zu geben: File → New → File from Template… → SwiftUI View.

Daten an Views übergeben

Weil Views Structs sind, übergibst du Daten über ihre Eigenschaften, ganz wie in Lektion 1.12:

Swift
struct GreetingView: View {
    let name: String

    var body: some View {
        Text("Hallo, \(name)!")
    }
}

// Verwendung:
GreetingView(name: "Anna")

Swift erzeugt automatisch einen Initializer mit allen Eigenschaften. Mehr ist nicht nötig.

Die Dateien eines neuen Projekts

Schau dir im Navigator an, was Xcode für dich angelegt hat:

Datei / OrdnerZweck
AnatomieApp.swiftStartpunkt mit @main
ContentView.swiftDie erste View
AssetsBilder, Farben und das App-Symbol (Lektion 2.6)
Projektdatei (blaues Symbol ganz oben)Einstellungen: Name, Version, unterstützte iOS-Version, Signierung

Klick auf das blaue Projektsymbol und dann auf dein Target „Anatomie“. Unter General findest du unter anderem:

  • Display Name: der Name unter dem App-Symbol
  • Minimum Deployments: die älteste iOS-Version, auf der deine App läuft
  • Version und Build: wichtig für die Veröffentlichung (Lektion 4.21)

Tipp

Ein Target ist ein „Bauziel“: eine App, ein Widget oder ein Test-Paket. Ein Projekt kann mehrere Targets haben. Am Anfang gibt es genau eins, deine App.

Der Lebenszyklus: Vorder- und Hintergrund

Eine App ist nicht einfach „an“ oder „aus“. Wischt der Nutzer zum Home-Bildschirm, geht die App in den Hintergrund. Holt er sie zurück, ist sie wieder aktiv. Manchmal willst du darauf reagieren, etwa um beim Verlassen Daten zu speichern.

SwiftUI stellt dafür den Wert scenePhase in der Umgebung (englisch environment) bereit. Die Umgebung ist eine Art Infotafel, die SwiftUI allen Views zur Verfügung stellt. Mehr dazu in Lektion 3.3.

Swift
struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Hallo")
            .onChange(of: scenePhase) { oldPhase, newPhase in
                switch newPhase {
                case .active:
                    print("App ist aktiv")
                case .inactive:
                    print("App ist inaktiv")
                case .background:
                    print("App ist im Hintergrund")
                @unknown default:
                    break
                }
            }
    }
}
  • @Environment(\.scenePhase) liest den aktuellen Zustand aus der Umgebung.
  • .onChange(of:) führt eine Closure aus, sobald sich ein Wert ändert. Sie bekommt den alten und den neuen Wert.
  • .inactive ist ein Übergangszustand, etwa wenn das Kontrollzentrum aufgezogen wird.
  • @unknown default fängt Fälle ab, die Apple in Zukunft vielleicht hinzufügt.

Erwartetes Ergebnis

Starte die App im Simulator und wechsle mit ⇧ ⌘ H zum Home-Bildschirm. In der Konsole erscheinen nacheinander „App ist inaktiv“ und „App ist im Hintergrund“. Öffnest du die App wieder, erscheint „App ist aktiv“.

Den print-Aufruf in einer Closure darfst du hier schreiben, weil .onChange eine Reaktion ist und nicht Teil der Beschreibung.

Code beim Start ausführen

Manchmal soll beim Start der App einmalig etwas passieren. Dafür kannst du der App-Struct einen init geben:

Swift
@main
struct AnatomieApp: App {
    init() {
        print("Die App startet")
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Für Dinge, die eine bestimmte View betreffen, gibt es den Modifier .onAppear { ... } und, für Aufgaben, die etwas dauern, .task { ... } (Lektion 3.7).

Typische Fehler

Mehrere @main

Fehlermeldung

'main' attribute can only apply to one type in a module

Es gibt mehr als einen Typ mit @main. Entferne das Attribut bei allen bis auf einen.

View nicht gefunden

Fehlermeldung

Cannot find 'WelcomeView' in scope

Die View existiert nicht, ist falsch geschrieben, oder die Datei gehört nicht zum Target. Prüf im File Inspector (rechte Seitenleiste) unter Target Membership, ob das Häkchen bei deiner App gesetzt ist.

body fehlt

Fehlermeldung

Type 'HeaderView' does not conform to protocol 'View'

Jede View braucht eine Eigenschaft var body: some View. Achte auf die genaue Schreibweise.

some View statt some Scene

Fehlermeldung

Type 'AnatomieApp' does not conform to protocol 'App'

In der App-Struct heißt es var body: some Scene, nicht some View.

FAQ

Anfänger

Was bedeutet das überhaupt?

Kann ich die Datei ContentView.swift umbenennen? Ja. Am saubersten geht das mit einem Rechtsklick auf den Namen ContentView im Code und Refactor → Rename. Dann ändert Xcode den Namen überall, auch den Dateinamen.

Muss jede View View im Namen haben? Nein, das ist nur eine Konvention, die viele Entwickler mögen, weil man sofort sieht, worum es sich handelt. Header oder ProfileCard sind genauso gültig.

Warum steht import SwiftUI in jeder Datei? Jede Swift-Datei muss selbst angeben, welche Frameworks sie benutzt. Fehlt die Zeile, kennt die Datei Typen wie View oder Text nicht.

Fortgeschritten

Warum verhält es sich so?

Gibt es neben WindowGroup andere Scenes? Ja. Settings für ein Einstellungsfenster auf dem Mac, DocumentGroup für dokumentbasierte Apps oder MenuBarExtra für Mac-Apps in der Menüleiste. Auf dem iPhone brauchst du in der Regel nur WindowGroup.

Wo lege ich Daten ab, die die ganze App betreffen? Meist erzeugt man sie in der App-Struct und reicht sie über die Umgebung an alle Views weiter, etwa mit .environment(...) oder .modelContainer(...) für SwiftData. Das lernst du in den Lektionen 3.3 und 3.14.

Was passiert mit meiner App im Hintergrund? iOS friert sie nach kurzer Zeit ein und kann sie bei Speichermangel jederzeit ohne Vorwarnung beenden. Wichtige Daten sollten deshalb spätestens beim Wechsel in .background gespeichert sein.

Profi

Was passiert unter der Haube?

Wie entsteht aus @main ein laufendes Programm? Das App-Protokoll stellt eine statische Methode main() bereit. @main sorgt dafür, dass der Compiler diese Methode als Einstiegspunkt verwendet. Sie startet die Laufzeitumgebung von SwiftUI, die wiederum die Scenes aus deinem body erzeugt und mit dem System verbindet.

Was ist der Unterschied zwischen scenePhase in der App und in einer View? Liest du scenePhase in der App-Struct, bekommst du einen zusammengefassten Zustand aller Scenes. In einer View bekommst du den Zustand der Scene, in der diese View steckt. Auf dem iPhone mit nur einer Scene ist das meist dasselbe, auf dem iPad mit mehreren Fenstern nicht.

Übungen

Anfänger

Übung 1: Andere Start-View

Leg eine neue View WelcomeView an, die „Willkommen!“ in großer Schrift zeigt. Ändere die App so, dass sie mit WelcomeView statt ContentView startet.

Lösung Übung 1Erst selbst probieren!
Swift
struct WelcomeView: View {
    var body: some View {
        Text("Willkommen!")
            .font(.largeTitle)
    }
}

@main
struct AnatomieApp: App {
    var body: some Scene {
        WindowGroup {
            WelcomeView()
        }
    }
}
Fortgeschritten

Übung 2: Verschachtelte Views

Baue eine ProfileView, die untereinander eine AvatarView (ein großes Symbol person.circle.fill) und eine NameView zeigt. NameView bekommt den Namen als Eigenschaft übergeben. Zeichne den View-Baum auf.

Lösung Übung 2Erst selbst probieren!
Swift
struct ProfileView: View {
    var body: some View {
        VStack {
            AvatarView()
            NameView(name: "Anna Berger")
        }
    }
}

struct AvatarView: View {
    var body: some View {
        Image(systemName: "person.circle.fill")
            .font(.system(size: 80))
    }
}

struct NameView: View {
    let name: String

    var body: some View {
        Text(name)
            .font(.title)
    }
}
Text
ProfileView
 └─ VStack
     ├─ AvatarView
     │   └─ Image
     └─ NameView
         └─ Text
Profi

Übung 3: Zeit im Hintergrund

Zähle mit scenePhase, wie oft die App in den Hintergrund gewechselt ist, und zeig die Zahl an. Tipp: Du brauchst @State aus Lektion 2.9. Schau ruhig voraus, falls du die Lektion noch nicht kennst.

Lösung Übung 3Erst selbst probieren!
Swift
struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase
    @State private var backgroundCount = 0

    var body: some View {
        Text("Im Hintergrund gewesen: \(backgroundCount)-mal")
            .onChange(of: scenePhase) { _, newPhase in
                if newPhase == .background {
                    backgroundCount += 1
                }
            }
    }
}

Den alten Wert brauchen wir nicht, deshalb steht dort _.

Zusammenfassung

  • Eine App besteht aus App → Scene → Views.
  • @main markiert den Startpunkt. Es gibt ihn genau einmal.
  • Die App-Struct erfüllt App und liefert in body eine Scene, meist WindowGroup.
  • Views bilden einen Baum. Eigene Views verwendest du wie Apples Views.
  • Daten übergibst du Views über Eigenschaften.
  • scenePhase verrät, ob die App aktiv, inaktiv oder im Hintergrund ist.

Spickzettel

Swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

struct GreetingView: View {
    let name: String
    var body: some View { Text("Hallo, \(name)") }
}

@Environment(\.scenePhase) private var scenePhase
.onChange(of: scenePhase) { old, new in ... }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.