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
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 SwiftDataholt das Framework.@Modelist 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:
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
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
@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:
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
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:
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:
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:
#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:
@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: trueschreibt nichts auf die Festplatte.container.mainContextist 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
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
Ü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!
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.
Ü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.
Ü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.
@Modelmacht eine Klasse speicherbar und beobachtbar.idundIdentifiablegibt es automatisch..modelContainer(for:)in der App-Struct richtet den Speicher ein.@Queryholt Daten und hält Views automatisch aktuell, mitsort:undorder:.@Environment(\.modelContext)fürinsertunddelete. Gespeichert wird automatisch.- Ändern: Eigenschaften einfach setzen, für Bindings
@Bindable. - Previews brauchen einen Container im Arbeitsspeicher.
Spickzettel
@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) }