Lektion 3.5 · Stufe 3 – Fortgeschritten

Architektur: MVVM, und wann du es nicht brauchst

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kennst du das Architekturmuster MVVM, weißt, wie es in SwiftUI mit @Observable aussieht, und kannst einschätzen, wann es dir hilft und wann es nur unnötigen Code erzeugt. Du lernst eine pragmatische Ordnung für deine Projekte kennen.

Das Problem: Wohin mit dem Code?

Je größer eine App wird, desto häufiger stellst du dir Fragen wie:

  • Gehört diese Berechnung in die View oder woanders hin?
  • Wo lade ich die Daten aus dem Internet?
  • Wie teste ich, ob meine Logik stimmt, ohne jedes Mal die App zu starten?

Darauf gibt es keine einzige richtige Antwort, aber bewährte Muster. Das bekannteste in der Apple-Welt heißt MVVM. Es wird so oft empfohlen, dass viele Anfänger glauben, sie müssten es verwenden. Diese Lektion zeigt dir, was dahintersteckt, und wann es sinnvoll ist.

Was ist Architektur?

Architektur heißt in der Softwareentwicklung: Wie teilst du den Code auf, und wer darf mit wem sprechen? Gute Architektur sorgt dafür, dass

  • jeder Teil eine klare Aufgabe hat,
  • du Teile unabhängig ändern kannst,
  • du Logik testen kannst, ohne die Oberfläche zu brauchen,
  • sich auch ein anderer im Code zurechtfindet.

Schlechte Architektur merkt man daran, dass jede kleine Änderung an fünf Stellen etwas kaputt macht.

MVVM in drei Teilen

MVVM steht für Model – View – ViewModel:

TeilAufgabeIn SwiftUI
Modeldie reinen Daten und Regeln der AppStructs wie Habit, Book, Expense
Viewzeigt Daten an und meldet Aktionen des NutzersSwiftUI-Views
ViewModelbereitet Daten für eine View auf und enthält ihre Logikeine @Observable-Klasse pro Bildschirm
Text
┌──────────┐   liest / ruft auf   ┌─────────────┐   verwendet   ┌─────────┐
│   View   │ ───────────────────▶ │  ViewModel  │ ────────────▶ │  Model  │
└──────────┘ ◀─────────────────── └─────────────┘               └─────────┘
               beobachtet Änderungen

Die View kennt das ViewModel. Das ViewModel kennt das Model. Aber das ViewModel kennt keine View. Es weiß nicht, ob seine Daten als Liste, als Raster oder gar nicht angezeigt werden.

Ein Beispiel

Ein Bildschirm, der Bücher anzeigt, filtert und sortiert:

Model:

Swift
struct Book: Identifiable {
    let id = UUID()
    var title: String
    var author: String
    var isRead: Bool
}

ViewModel:

Swift
@Observable
class BookListViewModel {
    var books: [Book] = []
    var searchText = ""
    var showOnlyUnread = false

    var visibleBooks: [Book] {
        books
            .filter { !showOnlyUnread || !$0.isRead }
            .filter { searchText.isEmpty || $0.title.localizedCaseInsensitiveContains(searchText) }
            .sorted { $0.title < $1.title }
    }

    var summary: String {
        let unread = books.filter { !$0.isRead }.count
        return "\(unread) von \(books.count) ungelesen"
    }

    func toggleRead(_ book: Book) {
        guard let index = books.firstIndex(where: { $0.id == book.id }) else { return }
        books[index].isRead.toggle()
    }
}

View:

Swift
struct BookListView: View {
    @State private var viewModel = BookListViewModel()

    var body: some View {
        NavigationStack {
            List {
                Toggle("Nur ungelesene", isOn: $viewModel.showOnlyUnread)
                Section(viewModel.summary) {
                    ForEach(viewModel.visibleBooks) { book in
                        Button(book.title) {
                            viewModel.toggleRead(book)
                        }
                    }
                }
            }
            .navigationTitle("Bücher")
        }
    }
}

Die View enthält keine einzige Zeile Logik mehr. Filtern, Sortieren, Zusammenfassen, Umschalten: alles steht im ViewModel.

Tipp

localizedCaseInsensitiveContains sucht in einem Text, ohne auf Groß- und Kleinschreibung zu achten, und berücksichtigt die Sprache des Geräts. Für Suchfunktionen ist das fast immer die richtige Wahl.

Der große Vorteil: Testbarkeit

Weil das ViewModel nichts von SwiftUI weiß, kannst du es ohne Oberfläche testen:

Swift
let viewModel = BookListViewModel()
viewModel.books = [
    Book(title: "Momo", author: "Ende", isRead: true),
    Book(title: "Krabat", author: "Preußler", isRead: false)
]
viewModel.showOnlyUnread = true

print(viewModel.visibleBooks.map(\.title))   // ["Krabat"]
print(viewModel.summary)                      // "1 von 2 ungelesen"

Das könntest du sogar in einem Playground ausprobieren. In Lektion 4.10 lernst du, solche Prüfungen als automatische Tests zu schreiben, die bei jeder Änderung laufen.

map(\.title) ist übrigens eine Kurzform für map { $0.title }, ein Key Path als Funktion.

Wann du MVVM nicht brauchst

Jetzt der wichtige Teil: MVVM ist kein Muss. In SwiftUI ist die View selbst schon sehr leichtgewichtig und deklarativ. Für viele Bildschirme wäre ein eigenes ViewModel reiner Mehraufwand.

Kein ViewModel nötig, wenn:

  • die View nur anzeigt, was sie bekommt,
  • die Logik aus ein, zwei Zeilen besteht,
  • der Zustand nur diese View betrifft, etwa ein offenes Sheet,
  • die Daten bereits aus einem geteilten Store oder aus SwiftData kommen.
Swift
// Braucht kein ViewModel
struct BookRow: View {
    let book: Book

    var body: some View {
        VStack(alignment: .leading) {
            Text(book.title).font(.headline)
            Text(book.author).foregroundStyle(.secondary)
        }
    }
}

Die ViewModel-Falle

Ein häufiges Muster bei Einsteigern: jede View bekommt ein ViewModel, auch die Zeile mit zwei Texten. Das Ergebnis sind doppelt so viele Dateien, Daten werden zwischen ViewModels hin- und herkopiert, und die eigentliche Stärke von SwiftUI, Views direkt an Daten zu binden, geht verloren. Architektur soll Probleme lösen, nicht schaffen.

Ein pragmatischer Ansatz

Für die meisten Apps in diesem Kurs bewährt sich diese Aufteilung:

  1. Models sind Structs (oder später SwiftData-Klassen) mit Daten und kleinen Regeln, wie Habit.isDoneToday oder Habit.streak.
  2. Ein oder wenige Stores (@Observable) verwalten die Daten der App und bieten Methoden zum Ändern an. Sie liegen meist in der Umgebung (Lektion 3.3).
  3. Views lesen aus Stores und Models und rufen deren Methoden auf.
  4. Ein ViewModel für einen Bildschirm kommt erst dazu, wenn dieser Bildschirm eigene, nicht triviale Logik hat, etwa Laden aus dem Internet, komplexes Filtern oder mehrstufige Formulare.

Faustregel

Fang ohne ViewModel an. Wenn eine View unübersichtlich wird, weil zu viel Logik darin steht, oder du die Logik testen willst, zieh sie in ein ViewModel um. Das geht in wenigen Minuten.

Projektstruktur

Unabhängig vom Muster hilft eine klare Ordnerstruktur. Ein bewährter Vorschlag:

Text
MyApp/
 ├─ App/
 │   └─ MyApp.swift
 ├─ Models/
 │   ├─ Book.swift
 │   └─ BookStore.swift
 ├─ Features/
 │   ├─ BookList/
 │   │   ├─ BookListView.swift
 │   │   ├─ BookListViewModel.swift
 │   │   └─ BookRow.swift
 │   └─ Settings/
 │       └─ SettingsView.swift
 ├─ Components/
 │   └─ Card.swift
 └─ Resources/
     └─ Assets.xcassets

Ordnen nach Funktionen (Features) statt nach Typen hat einen großen Vorteil: Alles, was zu einem Bildschirm gehört, liegt beieinander. Willst du die Bücherliste ändern, musst du nicht in drei Ordnern suchen.

Typische Fehler

ViewModel kennt die View

Swift
@Observable
class LoginViewModel {
    var errorText: Text?   // ⚠️ SwiftUI-Typ im ViewModel
}

Das ViewModel sollte keine Views oder SwiftUI-Typen enthalten. Speichere einen String, und die View macht daraus einen Text.

Daten doppelt halten

Zwei ViewModels haben je eine eigene Kopie derselben Bücherliste. Ändert sich die eine, ist die andere veraltet. Geteilte Daten gehören in einen Store, ViewModels greifen darauf zu.

ViewModel ohne @State

Swift
let viewModel = BookListViewModel()   // ⚠️

Wie jedes eigene Objekt: Die View, die das ViewModel erzeugt, hält es mit @State (Lektion 3.1).

FAQ

Anfänger

Was bedeutet das überhaupt?

Muss ich MVVM jetzt in jeder App verwenden? Nein. Du solltest es kennen und erkennen, weil es sehr verbreitet ist. Einsetzen solltest du es dort, wo es dir hilft: bei Bildschirmen mit echter Logik.

Was ist der Unterschied zwischen Store und ViewModel? Ein Store verwaltet Daten, die viele Bildschirme brauchen, und lebt meist so lange wie die App. Ein ViewModel gehört zu genau einem Bildschirm und bereitet Daten für ihn auf. In kleinen Apps verschwimmt die Grenze, und das ist in Ordnung.

Fortgeschritten

Warum verhält es sich so?

Wie bekommt ein ViewModel Daten aus einem Store in der Umgebung? Die View holt den Store mit @Environment und übergibt ihn dem ViewModel, zum Beispiel in .task oder über einen Initializer-Parameter. Das ViewModel selbst kann nicht auf die Umgebung zugreifen, weil es keine View ist.

Gibt es Alternativen zu MVVM? Ja, zum Beispiel MV (Model-View, ohne eigene ViewModels, wie im pragmatischen Ansatz oben), TCA (The Composable Architecture, eine Bibliothek mit strengen Regeln) oder Clean Architecture mit vielen Schichten. Alle haben Vor- und Nachteile. Für den Einstieg ist der pragmatische Ansatz meist die beste Wahl.

Profi

Was passiert unter der Haube?

Warum funktioniert MVVM in SwiftUI anders als in UIKit? In UIKit hatte die View (bzw. der View Controller) eigenen Zustand und musste manuell aktualisiert werden. Das ViewModel war dort vor allem ein Weg, Logik aus riesigen View Controllern herauszuholen. In SwiftUI ist die View bereits eine reine Funktion ihres Zustands. Ein ViewModel bringt deshalb vor allem Testbarkeit und Ordnung für komplexe Logik, aber keine grundsätzlich neue Fähigkeit.

Sollte ein ViewModel @MainActor sein? In den meisten Fällen ja, weil es Zustand hält, den die Oberfläche liest, und die Oberfläche nur auf dem Main Thread aktualisiert werden darf. Neuere Projektvorlagen machen das sogar automatisch für den ganzen Code. Was das genau bedeutet, lernst du in den Lektionen 3.7 und 3.8.

Übungen

Anfänger

Übung 1: Wer macht was?

Ordne zu, ob die Aufgabe ins Model, ins ViewModel oder in die View gehört:

  1. Die Farbe eines Buttons festlegen.
  2. Berechnen, ob eine Aufgabe überfällig ist.
  3. Aus 200 Aufgaben die heute fälligen herausfiltern und sortieren.
  4. Ein Sheet öffnen, wenn der Nutzer auf „+“ tippt.
  5. Den Titel einer Aufgabe speichern.
Lösung Übung 1Erst selbst probieren!
  1. View: Darstellung.
  2. Model, z. B. eine berechnete Eigenschaft isOverdue an der Aufgabe.
  3. ViewModel, oder Store: Aufbereitung für einen Bildschirm.
  4. View: reiner UI-Zustand mit @State.
  5. Model: die Eigenschaft title.
Fortgeschritten

Übung 2: Logik herausziehen

Nimm die HabitListView aus Projekt C und zieh doneCount, das Hinzufügen und das Löschen in ein @Observable-ViewModel HabitListViewModel. Die View soll danach keine Array-Operationen mehr enthalten.

Profi

Übung 3: Logik prüfen

Schreib für das BookListViewModel aus dieser Lektion im Playground fünf Prüfungen mit print oder assert: leere Liste, Suche mit Groß- und Kleinschreibung, Filter „nur ungelesene“, Kombination aus Suche und Filter, Sortierung. Tipp: assert(bedingung) hält das Programm an, wenn die Bedingung false ist.

Zusammenfassung

  • Architektur bestimmt, wie Code aufgeteilt ist und wer mit wem spricht.
  • MVVM: Model (Daten), View (Darstellung), ViewModel (Aufbereitung und Logik für einen Bildschirm).
  • Das ViewModel ist eine @Observable-Klasse, die keine Views kennt und sich dadurch gut testen lässt.
  • Nicht jede View braucht ein ViewModel. Fang einfach an und zieh Logik heraus, wenn es nötig wird.
  • Ordne Projekte nach Features statt nach Dateitypen.

Spickzettel

Swift
// Model
struct Book: Identifiable { let id = UUID(); var title: String; var isRead: Bool }

// ViewModel
@Observable
class BookListViewModel {
    var books: [Book] = []
    var searchText = ""
    var visibleBooks: [Book] { books.filter { searchText.isEmpty || $0.title.localizedCaseInsensitiveContains(searchText) } }
    func toggleRead(_ book: Book) { ... }
}

// View
struct BookListView: View {
    @State private var viewModel = BookListViewModel()
    var body: some View { List(viewModel.visibleBooks) { ... } }
}

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.