İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

SwiftUI: Erste App entwickeln – Schritt für Schritt

Ahmet Balaman

7 Min. Lesezeit

SwiftSwiftUIiOSXcodeStateList
SwiftUI: Erste App entwickeln – Schritt für Schritt

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.

  1. Xcode öffnen und Create New Project wählen.
  2. Oben iOS und als Vorlage App auswählen.
  3. Als Product Name TodoList eintragen. Interface: SwiftUI, Language: Swift.
  4. Storage auf None lassen; in der ersten App fassen wir keine Datenbank an.
  5. 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. @main bedeutet „hier startet das Programm“ und kommt pro Projekt genau einmal vor.
  • Scene: ein Fenster der App. Auf dem iPhone steht WindowGroup für ein einzelnes Vollbildfenster; auf iPad und Mac kann derselbe Code mehrere Fenster öffnen.
  • View: alles, was Sie auf dem Bildschirm sehen. ContentView ist 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:

  1. iPhone per Kabel anschließen und die Frage „Diesem Computer vertrauen“ bestätigen.
  2. 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.
  3. Auf dem iPhone Einstellungen > Datenschutz & Sicherheit > Entwicklermodus aktivieren.
  4. 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 $.

Kommentare