What You'll Build
A single-screen notes list with a "+" toolbar button that presents a sheet for adding a note, swipe-to-delete, and Core Data persistence so notes are still there after a relaunch.
Prerequisites
- Xcode with a new project created using the "App" template, "Use Core Data" checked (or add the stack manually as shown below)
- Basic Swift syntax — variables, structs, functions
- Helpful but not required: read SwiftUI Views & Modifiers and Core Data Basics first
Step by Step
1 Define the Note entity in the data model
Open your .xcdatamodeld file (or create one), add an entity named Note, and give it two attributes: title (String) and body (String), plus createdAt (Date). Xcode generates a matching NSManagedObject subclass automatically.
// Entity: Note
// Attributes: title (String), body (String), createdAt (Date)
// Codegen: Class Definition (default) — Xcode generates the Note class for you
2 Set up the persistence controller
If your project wasn't created with "Use Core Data" checked, add this small controller that owns the NSPersistentContainer.
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "NotesModel") // matches .xcdatamodeld file name
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data failed to load: \(error)") }
}
}
}
Inject the context into the SwiftUI environment in your App struct:
// NotesApp.swift
@main
struct NotesApp: App {
let persistence = PersistenceController.shared
var body: some Scene {
WindowGroup {
NotesListView()
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
}
3 Fetch and display notes with @FetchRequest
@FetchRequest binds a live Core Data query directly to a SwiftUI view — no manual refresh needed, the list updates automatically whenever the store changes.
// NotesListView.swift
import SwiftUI
import CoreData
struct NotesListView: View {
@Environment(\.managedObjectContext) private var context
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Note.createdAt, ascending: false)]
) private var notes: FetchedResults<Note>
@State private var showingAddNote = false
var body: some View {
NavigationStack {
Group {
if notes.isEmpty {
ContentUnavailableView("No notes yet", systemImage: "note.text", description: Text("Tap + to add one"))
} else {
List {
ForEach(notes) { note in
VStack(alignment: .leading) {
Text(note.title ?? "").font(.headline)
Text(note.body ?? "").font(.subheadline).foregroundColor(.secondary)
}
}
.onDelete(perform: deleteNotes)
}
}
}
.navigationTitle("My Notes")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showingAddNote = true }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddNote) {
AddNoteView()
}
}
}
private func deleteNotes(at offsets: IndexSet) {
offsets.map { notes[$0] }.forEach(context.delete)
try? context.save()
}
}
4 Build the "Add Note" sheet
// AddNoteView.swift
struct AddNoteView: View {
@Environment(\.managedObjectContext) private var context
@Environment(\.dismiss) private var dismiss
@State private var title = ""
@State private var body = ""
var body: some View {
NavigationStack {
Form {
TextField("Title", text: $title)
TextField("Body", text: $body)
}
.navigationTitle("New Note")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") { save() }.disabled(title.isEmpty)
}
}
}
}
private func save() {
let note = Note(context: context)
note.title = title
note.body = body
note.createdAt = Date()
try? context.save()
dismiss()
}
}
Because @FetchRequest observes the context, saving here immediately updates the list back on NotesListView — there's no callback or notification to wire up manually.
5 Run it
Build and run. Add a couple of notes, delete one with swipe-to-delete, then stop and relaunch the app — your remaining notes should still be there, proving Core Data persisted them to disk.
Final Working Code
The data model (Note entity), Persistence.swift, NotesApp.swift, NotesListView.swift, and AddNoteView.swift above together form the complete app exactly as written — create each file with a matching name and it will build as-is.