Skip to Content
Course content

156: Building a Local-First Notes App with SwiftData

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I define a Note model that SwiftData actually understands?

The magic happens with the @Model macro. In the old days of Core Data, you had to wrestle with a separate model editor file. Now, you just write a standard Swift class. I recommend keeping your properties simple—Strings and Dates are your best friends here.

import Foundation
import SwiftData

@Model 
class Note {
    var title: String
    var content: String
    var timestamp: Date
    
    init(title: String = "", content: String = "", timestamp: Date = .now) {
        self.title = title
        self.content = content
        self.timestamp = timestamp
    }
}

Once you've added @Model, SwiftData takes care of the persistence schema. But remember, for this to actually work in your app, you have to tell SwiftUI about it at the entry point. You'll use the .modelContainer(for: Note.self) modifier on your WindowGroup. If you forget this, your app will crash the second you try to save a note because there's no "bucket" to put the data in.

How do I get my list of notes to update automatically in the UI?

This is where @Query comes in. Instead of writing a fetch request and manually updating an array, you just declare the query at the top of your view. It acts like a live connection to your database.

struct NoteListView: View {
    @Query(sort: \Note.timestamp, order: .reverse) 
    private var notes: [Note]

    var body: some View {
        List(notes) { note in
            VStack(alignment: .leading) {
                Text(note.title).font(.headline)
                Text(note.content).lineLimit(1).font(.subheadline)
            }
        }
    }
}

I love this because it removes all the boilerplate. When you add a new note or delete one, the notes array updates automatically, and SwiftUI triggers a re-render. It feels almost like using a state variable, but the data is actually living on the disk.

What's the cleanest way to handle creating and deleting notes?

You need the modelContext. Think of the context as your "scratchpad." You make changes there first, and SwiftData handles the actual writing to the database. You can grab this context from the environment in any view.

struct NoteDetailView: View {
    @Environment(\.modelContext) private var modelContext
    @Bindable var note: Note

    var body: some View {
        Form {
            TextField("Title", text: $note.title)
            TextEditor(text: $note.content)
            
            Button("Delete Note", role: .destructive) {
                modelContext.delete(note)
            }
        }
    }
}

One thing I've noticed is that beginners often wonder where the save() call is. In most SwiftUI setups, SwiftData autosaves. You don't usually need to call try modelContext.save() manually unless you're doing something complex or need to ensure the data is written before a specific background task begins.

Can I filter these notes based on a search term?

Yes, but @Query is a bit rigid because its parameters must be constant at compile-time. If you want a dynamic search bar, you can't just pass a variable into the @Query macro inside the same view. The pro move here is to wrap your list in a sub-view and pass the search text into that sub-view's initializer.

Inside that sub-view, you initialize the @Query using a #Predicate. It looks like this:

init(searchString: String) {
    let predicate = #Predicate<Note> { note in
        note.title.contains(searchString) || note.content.contains(searchString)
    }
    _notes = Query(filter: predicate, sort: \.timestamp, order: .reverse)
}

It's a slightly weird syntax (using the underscore _notes to access the Query wrapper itself), but it's the only way to make your local-first app feel responsive when the note count grows from ten to a thousand.




📋 Practical Task

Exercise: Implementing a "Pinned" Note Toggle

Currently, your notes are only sorted by date. Your task is to add a "Pinning" feature to the notes app.

  • Modify the Note model to include a Boolean property called isPinned.
  • Update the NoteListView to show a pin icon next to each note.
  • Add a toggle (or button) in the NoteDetailView that allows the user to flip the isPinned status.
  • Challenge: Update the @Query in your list view so that pinned notes always appear at the top of the list, regardless of their timestamp. (Hint: You'll need to provide an array of SortDescriptor objects to the query).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.