EthicsComputerCourses
Join training +91 90532 10052

Saving Data with SwiftData

Swift · Lesson 26 of 30 Advanced 7 min read Updated

Everything your apps have stored so far disappears when the app closes. A reading list, an expense tracker or a notes app must remember data between launches. Apple's modern answer is SwiftData, a framework (iOS 17 and later) that saves your Swift classes to a database on the device, with very little code.

SwiftData is built on the proven storage engine of Core Data, but you describe your data with plain Swift and macros. In this lesson you will define a model, set up storage, show saved data with @Query, insert, edit and delete records, filter with #Predicate, and link models with relationships.

Defining a model

Add the @Model macro to a class, and SwiftData can store it. Each stored property becomes a column in the database. Models must be classes, and like @Observable classes, SwiftUI updates views automatically when their properties change.

Swift
import Foundation
import SwiftData

@Model
final class Book {
    var title: String
    var author: String
    var rating: Int
    var isRead: Bool
    var addedOn: Date

    init(title: String, author: String, rating: Int = 3, isRead: Bool = false, addedOn: Date = .now) {
        self.title = title
        self.author = author
        self.rating = rating
        self.isRead = isRead
        self.addedOn = addedOn
    }
}

That is the whole database design. Supported property types include the basic types, Date, URL, UUID, Data, Codable structs and enums, and other models. Use @Attribute(.unique) on a property that must never repeat, and @Transient for a property that should not be saved.

Containers and contexts

A model container manages the database file. Attach one to your app's window with .modelContainer(for:). A model context is your working area: you insert, change and delete models through it, and it saves changes to the container. Here is the whole cycle in plain code, using an in-memory store so it starts empty every time:

Swift
import SwiftData

do {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try ModelContainer(for: Book.self, configurations: config)
    let context = ModelContext(container)

    context.insert(Book(title: "Godan", author: "Premchand", rating: 5))
    context.insert(Book(title: "Gitanjali", author: "Rabindranath Tagore", rating: 4))
    context.insert(Book(title: "Malgudi Days", author: "R. K. Narayan", rating: 3))
    try context.save()

    let favourites = FetchDescriptor<Book>(
        predicate: #Predicate { $0.rating >= 4 },
        sortBy: [SortDescriptor(\.title)]
    )
    print(try context.fetchCount(FetchDescriptor<Book>()))
    print(try context.fetch(favourites).map(\.title))
} catch {
    print("Storage error: \(error)")
}
Output
3
["Gitanjali", "Godan"]

A FetchDescriptor describes a query: an optional filter written with the #Predicate macro, and sort order with SortDescriptor. The predicate looks like a normal closure, but SwiftData translates it into an efficient database query. In an app you rarely create containers by hand like this; the modifier does it for you.

Showing and changing data in SwiftUI

Set up the container once in the App. Then any view can read models with @Query, which fetches them and keeps the view updated, and can reach the context through the environment.

Swift
import SwiftUI
import SwiftData

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

struct BookListView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \Book.addedOn, order: .reverse) private var books: [Book]

    var body: some View {
        NavigationStack {
            List {
                ForEach(books) { book in
                    NavigationLink(value: book) {
                        VStack(alignment: .leading) {
                            Text(book.title)
                                .font(.headline)
                            Text("\(book.author), rated \(book.rating)/5")
                                .foregroundStyle(.secondary)
                        }
                    }
                }
                .onDelete { offsets in
                    for index in offsets {
                        context.delete(books[index])
                    }
                }
            }
            .navigationTitle("My Books")
            .navigationDestination(for: Book.self) { book in
                BookEditor(book: book)
            }
            .toolbar {
                Button("Add", systemImage: "plus") {
                    context.insert(Book(title: "New book", author: "Unknown"))
                }
            }
        }
    }
}

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

The first launch shows an empty list titled "My Books" with a + button in the navigation bar. Each tap adds a "New book" row at the top (newest first, because of the sort). Swiping a row left and tapping Delete removes it. Close the app completely and open it again: the books are still there, because the container saves to a file on the device. SwiftData autosaves changes made in the main context, so you do not need to call save() here. The preview uses inMemory: true so it never touches real data.

Editing a model

Models are observable, so you can bind controls straight to their properties with @Bindable.

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

    var body: some View {
        Form {
            TextField("Title", text: $book.title)
            TextField("Author", text: $book.author)
            Stepper("Rating: \(book.rating)", value: $book.rating, in: 1...5)
            Toggle("Finished reading", isOn: $book.isRead)
        }
        .navigationTitle(book.title)
    }
}

Tapping a book opens a form with its title, author, a rating stepper and a "Finished reading" switch. Changing the title updates the navigation title as you type, and when you go back, the list row already shows the new title. There is no "save" button: edits go straight into the model, and SwiftData saves them.

Filtering, relationships and small settings

@Query can filter too. For a filter that depends on a value, build the query in the view's initializer:

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

    init(minimumRating: Int) {
        _books = Query(filter: #Predicate<Book> { $0.rating >= minimumRating },
                       sort: \Book.title)
    }

    var body: some View {
        List(books) { book in
            Text(book.title)
        }
    }
}

With the three books from earlier, TopRatedView(minimumRating: 4) shows a plain list with "Gitanjali" and "Godan". Writing _books sets up the query wrapper itself rather than the array.

Models can refer to each other. A Shelf model could declare @Relationship(deleteRule: .cascade) var books: [Book] = [], with var shelf: Shelf? in Book. SwiftData keeps both sides in sync, and .cascade deletes a shelf's books when the shelf is deleted.

For a few simple settings, such as the reader's name or a dark-mode switch, a database is too much. Use @AppStorage("readerName") private var readerName = "" in a view, which saves the value in UserDefaults and works like @State.

Common mistakes

  • Forgetting .modelContainer(for:). Without a container, @Query and modelContext have nothing to work with.
  • Making a model a struct. @Model requires a class.
  • Using real data in previews. Use an in-memory container for previews and tests.
  • Putting large files in the database. Store images as files and save their names, or mark large Data with @Attribute(.externalStorage).

Key points

  • @Model turns a Swift class into a stored model; properties become saved fields.
  • .modelContainer(for:) sets up storage for the app; the model context inserts, deletes and saves.
  • @Query fetches models with sorting and filtering and keeps views updated.
  • #Predicate and FetchDescriptor describe queries in Swift code.
  • Models are observable, so @Bindable edits them directly; changes are autosaved.
  • @AppStorage suits small settings; SwiftData replaces direct Core Data for new apps.

Exercise

Add a Note model for a study app with title, subject and createdOn. Write a list view that shows notes sorted by createdOn (newest first) with the subject in secondary text, a button that inserts a note titled "Revision plan" for subject "Maths", and swipe-to-delete. Describe what the user sees after adding two notes and deleting one.

Show solution
Swift
import SwiftUI
import SwiftData

@Model
final class Note {
    var title: String
    var subject: String
    var createdOn: Date

    init(title: String, subject: String, createdOn: Date = .now) {
        self.title = title
        self.subject = subject
        self.createdOn = createdOn
    }
}

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

    var body: some View {
        NavigationStack {
            List {
                ForEach(notes) { note in
                    VStack(alignment: .leading) {
                        Text(note.title)
                        Text(note.subject)
                            .foregroundStyle(.secondary)
                    }
                }
                .onDelete { offsets in
                    for index in offsets {
                        context.delete(notes[index])
                    }
                }
            }
            .navigationTitle("Notes")
            .toolbar {
                Button("Add", systemImage: "plus") {
                    context.insert(Note(title: "Revision plan", subject: "Maths"))
                }
            }
        }
    }
}

After two taps on +, the list shows two rows reading "Revision plan" with "Maths" in grey below. Swiping one away leaves a single row, and that row is still there after the app is closed and reopened, as long as the app's WindowGroup has .modelContainer(for: Note.self).

Quick Quiz

1. Which macro makes a class storable by SwiftData?

2. What does @Query(sort: \Book.title) in a view do?

3. How do you remove a book from storage?

4. What is the best place for a single "dark mode" setting?

Learn Swift with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call