Lektion 3.14 · Stufe 3 – Fortgeschritten

SwiftData I: Modelle und @Query

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du mit SwiftData Daten dauerhaft in einer Datenbank speichern: Modelle mit @Model definieren, einen Container mit .modelContainer einrichten, Daten mit @Query anzeigen und mit dem ModelContext einfügen und löschen. Außerdem richtest du Previews mit Beispieldaten ein.

Das Problem: Echte Daten brauchen eine Datenbank

Mit @AppStorage kannst du Einstellungen speichern. Aber der Habit-Tracker hat vielleicht 20 Gewohnheiten mit je Hunderten Einträgen. Eine Ausgaben-App sammelt über die Jahre Tausende Buchungen. Dafür ist UserDefaults nicht gemacht.

Du brauchst eine Datenbank: einen Speicher, der viele Datensätze effizient ablegt, schnell findet, sortiert und filtert, und der Änderungen sicher auf die Festplatte schreibt.

Apples Antwort darauf heißt SwiftData. Und das Besondere: Es fühlt sich kaum anders an als das, was du schon kennst.

Ein Modell mit @Model

Swift
import SwiftData

@Model
class Book {
    var title: String
    var author: String
    var pages: Int
    var isRead: Bool
    var addedAt: Date

    init(title: String, author: String, pages: Int, isRead: Bool = false) {
        self.title = title
        self.author = author
        self.pages = pages
        self.isRead = isRead
        self.addedAt = .now
    }
}
  • import SwiftData holt das Framework.
  • @Model ist ein Makro, wie @Observable (Lektion 3.1). Es macht die Klasse speicherbar und gleichzeitig beobachtbar: Views aktualisieren sich automatisch, wenn sich Eigenschaften ändern.
  • Modelle sind Klassen, keine Structs. Jeder Datensatz in der Datenbank ist ein bestimmtes Objekt mit einer Identität (Lektion 1.13).
  • Klassen brauchen einen eigenen init.

Eine id musst du nicht anlegen: SwiftData gibt jedem Objekt automatisch eine eindeutige Kennung, und @Model-Klassen erfüllen von selbst Identifiable. Listen und ForEach funktionieren also direkt.

Tipp

Welche Typen kann eine Eigenschaft haben? Alle einfachen Typen wie String, Int, Double, Bool, Date, URL, UUID, Data, deren Optionals, Arrays davon, Codable-Structs und -Enums sowie andere @Model-Klassen (Lektion 3.16).

Schritt 1: Den Container einrichten

SwiftData braucht einen Container: den eigentlichen Speicher, der die Datenbankdatei auf dem Gerät verwaltet. Du richtest ihn einmal in der App-Struct ein:

Swift
import SwiftUI
import SwiftData

@main
struct LibraryApp: App {
    var body: some Scene {
        WindowGroup {
            BookListView()
        }
        .modelContainer(for: Book.self)
    }
}

.modelContainer(for: Book.self) erledigt alles: Datenbank anlegen, beim nächsten Start wieder öffnen, das Modell bereitstellen. Der Container wird über die Umgebung (Lektion 3.3) an alle Views weitergegeben.

Hast du mehrere Modelle, übergibst du ein Array: .modelContainer(for: [Book.self, Author.self]).

Schritt 2: Daten anzeigen mit @Query

Swift
struct BookListView: View {
    @Query private var books: [Book]

    var body: some View {
        NavigationStack {
            List(books) { book in
                VStack(alignment: .leading) {
                    Text(book.title)
                        .font(.headline)
                    Text(book.author)
                        .foregroundStyle(.secondary)
                }
            }
            .navigationTitle("Bibliothek")
        }
    }
}

@Query holt alle Book-Objekte aus der Datenbank. Und das Beste: Die Liste bleibt automatisch aktuell. Wird irgendwo in der App ein Buch hinzugefügt, geändert oder gelöscht, aktualisiert sich jede View mit einem passenden @Query von selbst.

Sortieren

Swift
@Query(sort: \Book.title) private var books: [Book]
@Query(sort: \Book.addedAt, order: .reverse) private var newestFirst: [Book]

\Book.title ist ein Key Path (Lektion 2.16, 🟡-FAQ): „Sortiere nach der Eigenschaft title.“ Mit order: .reverse wird absteigend sortiert, hier die neuesten zuerst.

Schritt 3: Einfügen

Zum Ändern der Datenbank brauchst du den ModelContext. Er ist so etwas wie der Arbeitsbereich, in dem du Objekte einfügst, änderst und löschst. Du holst ihn aus der Umgebung:

Swift
struct BookListView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \Book.title) private var books: [Book]

    var body: some View {
        NavigationStack {
            List(books) { book in
                Text(book.title)
            }
            .navigationTitle("Bibliothek")
            .toolbar {
                Button("Neu", systemImage: "plus") {
                    let book = Book(title: "Neues Buch", author: "Unbekannt", pages: 100)
                    context.insert(book)
                }
            }
        }
    }
}

context.insert(book) legt das Objekt in der Datenbank ab. Die Liste zeigt es sofort an, weil @Query die Änderung bemerkt.

Erwartetes Ergebnis

Jeder Tipp auf „+“ fügt ein Buch hinzu. Beende die App ganz und starte sie neu: Alle Bücher sind noch da.

Und das Speichern?

Du musst nicht selbst speichern. SwiftData sichert Änderungen automatisch in kurzen Abständen und wenn die App in den Hintergrund geht. Für die allermeisten Fälle ist das genau richtig.

Willst du sicherstellen, dass etwas sofort geschrieben wird, etwa vor einer wichtigen Aktion, gibt es try context.save().

Schritt 4: Löschen

Swift
List {
    ForEach(books) { book in
        Text(book.title)
    }
    .onDelete { offsets in
        for index in offsets {
            context.delete(books[index])
        }
    }
}

context.delete(objekt) entfernt ein Objekt aus der Datenbank. onDelete liefert die Positionen der gewischten Zeilen (Lektion 2.16). Darüber holen wir die passenden Bücher aus dem @Query-Array.

Anders als bei einem Array mit @State löschst du hier nicht aus books. books ist nur eine Ansicht auf die Datenbank. Gelöscht wird in der Datenbank, und @Query zieht nach.

Schritt 5: Ändern

Ein @Model-Objekt änderst du einfach, wie jedes Objekt einer Klasse:

Swift
Button(book.isRead ? "Ungelesen" : "Gelesen") {
    book.isRead.toggle()
}

Das ist alles. SwiftData bemerkt die Änderung, speichert sie, und alle Views aktualisieren sich. Kein Binding nötig, kein „Element im Array suchen“ wie bei Structs (Lektion 2.16).

Für Bindings, etwa in einem Formular, verwendest du @Bindable (Lektion 3.2). Das funktioniert mit @Model-Objekten genauso wie mit @Observable:

Swift
struct BookEditor: View {
    @Bindable var book: Book

    var body: some View {
        Form {
            TextField("Titel", text: $book.title)
            TextField("Autor", text: $book.author)
            Stepper("Seiten: \(book.pages)", value: $book.pages, in: 1...5000)
            Toggle("Gelesen", isOn: $book.isRead)
        }
        .navigationTitle("Bearbeiten")
    }
}

Jede Eingabe landet direkt in der Datenbank. Mehr zum Bearbeiten, Sortieren und Filtern in der nächsten Lektion.

Previews mit Beispieldaten

Eine View mit @Query braucht auch in der Preview einen Container, sonst stürzt sie ab. Für Previews nimmst du einen Container im Arbeitsspeicher, der nach der Preview wieder verschwindet:

Swift
#Preview {
    BookListView()
        .modelContainer(for: Book.self, inMemory: true)
}

Die Liste ist dann leer. Mit Beispieldaten wird es aussagekräftiger. Dafür legst du einen Preview-Container an, der beim Erzeugen gleich ein paar Bücher einfügt:

Swift
@MainActor
let previewContainer: ModelContainer = {
    let container = try! ModelContainer(
        for: Book.self,
        configurations: ModelConfiguration(isStoredInMemoryOnly: true)
    )
    container.mainContext.insert(Book(title: "Momo", author: "Michael Ende", pages: 304))
    container.mainContext.insert(Book(title: "Krabat", author: "Otfried Preußler", pages: 320, isRead: true))
    return container
}()

#Preview {
    BookListView()
        .modelContainer(previewContainer)
}
  • ModelContainer(for:configurations:) erzeugt einen Container von Hand. isStoredInMemoryOnly: true schreibt nichts auf die Festplatte.
  • container.mainContext ist der Kontext für den Main Thread, derselbe, den Views über die Umgebung bekommen.
  • Das { … }() am Ende ist eine Closure, die sofort ausgeführt wird. So lässt sich eine Konstante mit mehreren Schritten vorbereiten.
  • Das try! ist hier vertretbar: Scheitert ein Container im Arbeitsspeicher, ist das ein Programmierfehler, und für Previews ist ein Absturz mit klarer Meldung sogar hilfreich.

Typische Fehler

Container fehlt

Fehlermeldung

Laufzeitfehler, etwa: Failed to find any currently loaded container for Book

.modelContainer fehlt in der App-Struct oder in der Preview. Jede View mit @Query oder modelContext braucht einen Container weiter oben.

Modell nicht im Container

Ein Modell wird verwendet, steht aber nicht in .modelContainer(for: ...). Ergänze es im Array. Verknüpfte Modelle (Lektion 3.16) werden meist automatisch mit aufgenommen.

@Model an einer Struct

Fehlermeldung

'@Model' cannot be applied to struct type 'Book'

SwiftData-Modelle sind immer Klassen.

Aus dem @Query-Array löschen

Swift
books.remove(atOffsets: offsets)

Fehlermeldung

Cannot use mutating member on immutable value: 'books' is a get-only property

@Query liefert eine schreibgeschützte Ansicht. Gelöscht wird über context.delete(...).

Modell geändert, App stürzt beim Start ab

Änderst du ein Modell grundlegend, etwa indem du eine Eigenschaft ohne Standardwert hinzufügst, passt die vorhandene Datenbank im Simulator nicht mehr dazu. Während der Entwicklung hilft es, die App im Simulator zu löschen. Für veröffentlichte Apps gibt es Migrationen (🔴-FAQ).

FAQ

Anfänger

Was bedeutet das überhaupt?

Wo wird die Datenbank gespeichert? In einer Datei im privaten Ordner deiner App auf dem Gerät. Andere Apps haben keinen Zugriff darauf. Wird die App gelöscht, wird auch die Datenbank gelöscht.

Was ist der Unterschied zwischen SwiftData und Core Data? Core Data ist Apples älteres Framework für denselben Zweck. SwiftData baut darauf auf, ist aber viel einfacher zu benutzen und für SwiftUI gemacht. Für neue Projekte nimm SwiftData.

Brauche ich Internet für SwiftData? Nein. Die Datenbank liegt komplett auf dem Gerät. Optional kann SwiftData Daten über iCloud zwischen Geräten synchronisieren.

Fortgeschritten

Warum verhält es sich so?

Wie zähle ich Objekte, ohne alle zu laden? @Query lädt Objekte erst, wenn sie gebraucht werden. Für eine reine Anzahl gibt es try context.fetchCount(FetchDescriptor<Book>()), das nur zählt.

Kann ich SwiftData mit iCloud synchronisieren? Ja. Mit der Fähigkeit iCloud und CloudKit unter Signing & Capabilities synchronisiert SwiftData automatisch. Dafür gelten aber Regeln, etwa dass alle Eigenschaften einen Standardwert haben oder optional sein müssen. Das braucht das kostenpflichtige Developer Program.

Wie mache ich eine Eigenschaft eindeutig? Mit @Attribute(.unique) var isbn: String. Fügt man ein Objekt mit derselben ISBN ein, ersetzt es das vorhandene, statt ein Duplikat anzulegen.

Profi

Was passiert unter der Haube?

Was passiert bei Modelländerungen in einer veröffentlichten App? Kleine Änderungen wie neue optionale Eigenschaften oder solche mit Standardwert übernimmt SwiftData automatisch (leichtgewichtige Migration). Für größere Änderungen wie Umbenennen oder Datenumwandlung beschreibst du die Versionen deines Modells mit VersionedSchema und den Übergang mit einem SchemaMigrationPlan. Plane das, bevor du eine Version veröffentlichst, deren Modell sich später grundlegend ändern könnte.

Ist ModelContext threadsicher? Nein. Ein Kontext und seine Objekte gehören zu dem Bereich, in dem sie erzeugt wurden, mainContext also zum Main Actor. Für Arbeit im Hintergrund, etwa das Importieren tausender Datensätze, legst du einen eigenen Kontext an, am besten in einem Typ mit dem Makro @ModelActor. Zwischen Bereichen gibst du dann nicht Objekte weiter, sondern deren persistentModelID (Lektion 3.8).

Übungen

Anfänger

Übung 1: Notizen

Baue eine Notizen-App mit einem @Model Note (Text und Erstellungsdatum). Ein Plus-Button legt eine Notiz mit dem aktuellen Datum an. Die Liste zeigt die neuesten zuerst und erlaubt Löschen per Wischgeste. Prüf, ob die Notizen einen Neustart überleben.

Lösung Übung 1Erst selbst probieren!
Swift
import SwiftUI
import SwiftData

@Model
class Note {
    var text: String
    var createdAt: Date

    init(text: String) {
        self.text = text
        self.createdAt = .now
    }
}

struct NotesView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \Note.createdAt, order: .reverse) private var notes: [Note]

    var body: some View {
        NavigationStack {
            List {
                ForEach(notes) { note in
                    VStack(alignment: .leading) {
                        Text(note.text)
                        Text(note.createdAt, style: .relative)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }
                .onDelete { offsets in
                    for index in offsets {
                        context.delete(notes[index])
                    }
                }
            }
            .navigationTitle("Notizen")
            .toolbar {
                Button("Neu", systemImage: "plus") {
                    context.insert(Note(text: "Notiz \(notes.count + 1)"))
                }
            }
        }
    }
}

#Preview {
    NotesView()
        .modelContainer(for: Note.self, inMemory: true)
}

In der App-Struct: .modelContainer(for: Note.self) an WindowGroup.

Fortgeschritten

Übung 2: Bearbeiten

Mach jede Notiz antippbar. Ein NavigationLink öffnet einen Editor mit einem mehrzeiligen Textfeld, das über @Bindable direkt in die Notiz schreibt. Prüf, ob Änderungen sofort in der Liste sichtbar sind und einen Neustart überleben.

Profi

Übung 3: Preview-Container

Leg einen Preview-Container mit zehn Beispielnotizen an, deren Erstellungsdaten über die letzten zehn Tage verteilt sind. Tipp: Calendar.current.date(byAdding: .day, value: -n, to: .now). Zeig damit die Liste in einer Preview.

Zusammenfassung

  • SwiftData speichert Daten dauerhaft in einer Datenbank auf dem Gerät.
  • @Model macht eine Klasse speicherbar und beobachtbar. id und Identifiable gibt es automatisch.
  • .modelContainer(for:) in der App-Struct richtet den Speicher ein.
  • @Query holt Daten und hält Views automatisch aktuell, mit sort: und order:.
  • @Environment(\.modelContext) für insert und delete. Gespeichert wird automatisch.
  • Ändern: Eigenschaften einfach setzen, für Bindings @Bindable.
  • Previews brauchen einen Container im Arbeitsspeicher.

Spickzettel

Swift
@Model
class Book {
    var title: String
    var isRead = false
    init(title: String) { self.title = title }
}

WindowGroup { ContentView() }
    .modelContainer(for: Book.self)

@Environment(\.modelContext) private var context
@Query(sort: \Book.title) private var books: [Book]

context.insert(Book(title: "Momo"))
context.delete(book)
book.isRead.toggle()

@Bindable var book: Book
TextField("Titel", text: $book.title)

#Preview { ContentView().modelContainer(for: Book.self, inMemory: true) }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.