SwiftUI: Erste App entwickeln – Schritt für Schritt
7 Min. Lesezeit

SwiftUI ist Apples UI-Framework, in dem Sie die Oberfläche im Code beschreiben: Sie schreiben, was auf dem Bildschirm stehen soll, und SwiftUI sorgt dafür, dass die Anzeige zu Ihren Daten passt. In diesem Beitrag gehen wir vom leeren Xcode-Projekt bis zu einer funktionierenden To-do-Liste. Es geht nicht ums Auswendiglernen, sondern darum zu sehen, wie App, View, @State, List und TextField zusammenhängen.
Wenn Ihnen die Swift-Syntax noch völlig fremd ist, lesen Sie zuerst den Beitrag zu Optionals und guard let; der Code unten verwendet guard und setzt voraus, dass Sie wissen, was ein Optional ist.
Das Projekt in Xcode anlegen
Sie brauchen einen Mac und Xcode, das kostenlos im App Store erhältlich ist. Für den Simulator ist kein kostenpflichtiger Entwickler-Account nötig.
- Xcode öffnen und Create New Project wählen.
- Oben iOS und als Vorlage App auswählen.
- Als Product Name
TodoListeintragen. Interface: SwiftUI, Language: Swift. - Storage auf None lassen; in der ersten App fassen wir keine Datenbank an.
- Ordner wählen und Projekt erstellen.
Xcode erzeugt zwei Swift-Dateien: TodoListApp.swift und ContentView.swift. Mehr ist die App vorerst nicht.
App, Scene und View: drei Ebenen
TodoListApp.swift ist der Einstiegspunkt:
import SwiftUI
@main
struct TodoListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Hier stecken drei Begriffe:
- App: die Anwendung selbst.
@mainbedeutet „hier startet das Programm“ und kommt pro Projekt genau einmal vor. - Scene: ein Fenster der App. Auf dem iPhone steht
WindowGroupfür ein einzelnes Vollbildfenster; auf iPad und Mac kann derselbe Code mehrere Fenster öffnen. - View: alles, was Sie auf dem Bildschirm sehen.
ContentViewist unser erster Screen.
Wer von Flutter kommt, findet die Entsprechung schnell: App ist grob runApp plus MaterialApp, eine View ist ein Widget. Jede View ist ein struct und muss lediglich einen body zurückgeben. Warum struct und nicht class, erkläre ich separat im Beitrag struct vs. class in Swift.
Layout mit VStack und HStack
Layout entsteht in SwiftUI durch verschachtelte Container. VStack ordnet seine Kinder untereinander an, HStack nebeneinander, ZStack übereinander.
struct HelloView: View {
var body: some View {
VStack(spacing: 12) {
Text("Hallo SwiftUI")
.font(.title)
HStack {
Image(systemName: "star.fill")
Text("Mein erster Screen")
}
}
.padding()
}
}Aufrufe wie .font(.title) und .padding() heißen Modifier. Jeder Modifier umschließt die View davor und liefert eine neue View zurück, deshalb spielt die Reihenfolge eine Rolle. .padding() gefolgt von .background(...) färbt auch den Abstand ein, umgekehrt nicht.
@State: den Bildschirm an Daten binden
Weil Views Structs sind, können sie ihre eigenen Eigenschaften innerhalb von body nicht ändern. Veränderliche Daten müssen Sie SwiftUI anvertrauen, und der einfachste Weg dafür ist @State.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Zähler: \(count)")
.font(.largeTitle)
Button("Erhöhen") {
count += 1
}
}
}
}Ändert sich count, berechnet SwiftUI body neu und aktualisiert nur das, was sich auf dem Bildschirm tatsächlich geändert hat. Kein setState, kein Label, das Sie von Hand aktualisieren. Halten Sie @State immer private: Diese Daten gehören nur dieser View.
Die To-do-Liste bauen
Zuerst das Modell. Sie können es oben in ContentView.swift einfügen:
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isDone = false
}Durch Identifiable kann SwiftUI die Zeilen einer Liste auseinanderhalten. Wird eine Zeile gelöscht oder verschoben, verfolgt SwiftUI über id, welche Zeile welche ist.
Nun der Screen selbst:
struct ContentView: View {
@State private var items: [TodoItem] = [
TodoItem(title: "Xcode installieren"),
TodoItem(title: "Ersten SwiftUI-Screen schreiben")
]
@State private var newTitle = ""
private var trimmedTitle: String {
newTitle.trimmingCharacters(in: .whitespaces)
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
HStack {
TextField("Neue Aufgabe", text: $newTitle)
.textFieldStyle(.roundedBorder)
.onSubmit(addItem)
Button("Hinzufügen", action: addItem)
.disabled(trimmedTitle.isEmpty)
}
.padding()
List {
ForEach($items) { $item in
TodoRow(item: $item)
}
.onDelete { offsets in
items.remove(atOffsets: offsets)
}
}
}
.navigationTitle("Aufgaben")
}
}
private func addItem() {
guard !trimmedTitle.isEmpty else { return }
items.append(TodoItem(title: trimmedTitle))
newTitle = ""
}
}Drei Details verdienen Aufmerksamkeit.
$newTitle: Ein TextField liest den Text nicht nur, es ändert ihn, während der Nutzer tippt. Deshalb will es nicht den Wert, sondern eine Zwei-Wege-Verbindung dorthin. Das vorangestellte $ heißt: „Gib mir das Binding dieses States.“
ForEach($items) { $item in … }: Wir durchlaufen das Array als Bindings, damit jede Zeile ihr eigenes Element ändern kann. Würden die Zeilen nur anzeigen, genügte ForEach(items) { item in … }.
.onDelete: Eine Zeile Code liefert das Löschen per Wischgeste. List kümmert sich außerdem um Scrollen, Trennlinien und die Wiederverwendung von Zeilen.
Die Zeile liegt in einer eigenen View:
struct TodoRow: View {
@Binding var item: TodoItem
var body: some View {
Button {
item.isDone.toggle()
} label: {
HStack {
Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isDone ? .green : .secondary)
Text(item.title)
.strikethrough(item.isDone)
.foregroundStyle(item.isDone ? .secondary : .primary)
Spacer()
}
}
.buttonStyle(.plain)
}
}@Binding ist für Views gedacht, denen die Daten nicht gehören, die sie aber ändern dürfen. Die Daten leben in ContentView, TodoRow leiht sie sich nur aus. Diese Trennung wird mit wachsender App wichtiger; die Fortsetzung finden Sie im Beitrag Datenfluss mit @State, @Binding und @Observable.
Previews: sehen, ohne zu starten
Fügen Sie am Dateiende Previews hinzu:
#Preview {
ContentView()
}
#Preview("Erledigte Zeile") {
TodoRow(item: .constant(TodoItem(title: "Beispielaufgabe", isDone: true)))
}Der Canvas rechts in Xcode aktualisiert sich beim Speichern. Ist er ausgeblendet, schalten Sie ihn über Editor > Canvas ein. Mit .constant(...) in der zweiten Preview übergeben Sie einer View, die ein Binding erwartet, einen festen Wert. Eine View in jedem ihrer Zustände als Preview anzulegen ist deutlich schneller, als sich im Simulator zu diesem Zustand durchzuklicken.
Im Simulator und auf dem Gerät ausführen
Wählen Sie im Gerätemenü oben im Xcode-Fenster einen iPhone-Simulator und starten Sie mit Cmd + R. Der erste Build dauert etwas, die folgenden gehen schnell.
So testen Sie auf dem eigenen iPhone:
- iPhone per Kabel anschließen und die Frage „Diesem Computer vertrauen“ bestätigen.
- In den Projekteinstellungen unter Signing & Capabilities Ihren Apple-Account als Team auswählen. Ein kostenloser Account reicht für Tests auf dem eigenen Gerät.
- Auf dem iPhone Einstellungen > Datenschutz & Sicherheit > Entwicklermodus aktivieren.
- Das iPhone im Gerätemenü auswählen und starten.
Um die App an andere zu verteilen, brauchen Sie die kostenpflichtige Mitgliedschaft im Apple Developer Program; diesen Teil beschreibe ich Schritt für Schritt in der Anleitung zur Veröffentlichung im App Store.
Wann verwenden – und wann nicht?
Für Einsteiger ist SwiftUI der richtige Startpunkt: Mit wenig Code kommen Sie zu Ergebnissen, und Apple liefert neue APIs zuerst für SwiftUI aus. In einem neuen Projekt sollte SwiftUI die Standardwahl sein.
Die Alternative heißt UIKit. Wenn Sie in einer älteren Codebasis arbeiten oder eine stark angepasste Komponente brauchen, zahlt sich UIKit-Wissen aus. Beides schließt sich nicht aus; eine UIKit-Komponente lässt sich über UIViewRepresentable in SwiftUI einbetten.
Wollen Sie dieselbe App aus einer Codebasis für iOS und Android ausliefern, ist SwiftUI das falsche Werkzeug, denn es läuft nur auf Apple-Plattformen. Dann ist ein Cross-Platform-Framework wie Flutter sinnvoller; die Optionen vergleiche ich in Flutter vs. React Native.
Häufige Fehler
1. Eine Eigenschaft ohne @State ändern
struct BadCounter: View {
var count = 0
var body: some View {
Button("Erhöhen") { count += 1 } // kompiliert nicht
}
}Symptom: ein Compilerfehler in der Art von Left side of mutating operator isn't mutable: 'self' is immutable. Lösung: als @State private var count = 0 deklarieren.
2. Dem TextField einen Wert ohne $ übergeben
Symptom: Cannot convert value 'text' of type 'String' to expected type 'Binding<String>'. Schreiben Sie text: $newTitle statt text: newTitle. Die Regel ist einfach: Soll das Steuerelement den Wert ändern, verlangt es ein Binding.
3. Identifiable am Modell vergessen
Symptom: In der Zeile mit ForEach oder List erscheint ein Fehler mit requires that 'TodoItem' conform to 'Identifiable'. Ergänzen Sie Identifiable und eine eindeutige id. Den Titel per id: \.self als Identität zu nehmen ist verlockend, doch sobald zwei Einträge denselben Titel haben, geraten die Zeilen durcheinander und die falsche wird gelöscht.
4. Alles in einen einzigen body schreiben
Symptom: The compiler is unable to type-check this expression in reasonable time oder schlicht ein unlesbarer body mit Hunderten Zeilen. Lösung: in kleine Views aufteilen, wie bei TodoRow. Kleine Views entlasten den Compiler und lassen sich einzeln als Preview anzeigen.
Wie es weitergeht
Schließen Sie die App, ist die Liste wieder auf dem Anfangsstand, denn die Daten liegen nur im Arbeitsspeicher. Naheliegende nächste Schritte: Daten dauerhaft speichern, mit NavigationLink auf einen zweiten Screen wechseln und Daten aus dem Netz laden. Für Letzteres ist der Leitfaden zu async/await eine gute Fortsetzung. Wenn Sie die Themen lieber der Reihe nach mit einem Lehrer durcharbeiten, finden Sie Details auf der Seite Swift-Unterricht.
Häufig gestellte Fragen
Muss ich vor SwiftUI erst UIKit lernen?
Nein. Sie können direkt mit SwiftUI beginnen; grundlegende Swift-Syntax genügt. UIKit wird relevant, wenn Sie an einem älteren Projekt arbeiten oder eine Komponente brauchen, die SwiftUI nicht abdeckt.
Brauche ich einen Mac, um eine SwiftUI-App zu entwickeln?
Ja. Xcode läuft nur unter macOS, und zum Bauen und Signieren einer iOS-App benötigen Sie Xcode. Die Sprache Swift selbst können Sie auch auf anderen Plattformen ausprobieren, für SwiftUI-Oberflächen und den Simulator ist jedoch ein Mac nötig.
Brauche ich einen kostenpflichtigen Account, um auf meinem eigenen iPhone zu testen?
Nein. Mit einem kostenlosen Apple-Account können Sie die App aus Xcode auf Ihr eigenes Gerät installieren. Das kostenpflichtige Apple Developer Program brauchen Sie erst, wenn Sie über TestFlight oder den App Store verteilen möchten.
Was ist der Unterschied zwischen @State und @Binding?
@State besitzt die Daten; diese View speichert den Wert. @Binding ist eine Zwei-Wege-Verbindung zu Daten, die einer anderen View gehören. Eine Kind-View, die Daten ändern soll, erhält ein @Binding, und die Eltern-View übergibt es mit $.
Verwandte Artikel
SwiftUI @State, @Binding und @Observable: Datenfluss erklärt
Wann @State, @Binding, @Observable und @Environment in SwiftUI passen, wie sie zu ObservableObject stehen, mit Entscheidungstabelle und typischen Fehlern.
Swift Closures erklärt: $0, @escaping und [weak self]
Die Closure-Syntax in Swift Schritt für Schritt von der Langform bis $0: Trailing Closures, Werte einfangen, @escaping, Retain Cycles und [weak self].
Swift async/await Leitfaden: Task, Actor und @MainActor
Vom Completion Handler zu async/await: Task, async let, TaskGroup, @MainActor, Actors, Abbruch und ein JSON-API-Aufruf mit URLSession in SwiftUI.