State, bindings, and navigation
Build the editor screen, save into SwiftData, and understand the data flow that connects your screens.
The editor is where state and persistence meet. The user types into @State, and on Save you insert a new Entry into the SwiftData modelContext. Because the list uses @Query, it updates automatically — you never manually refresh it.
import SwiftUI
import SwiftData
struct EntryEditorView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
// Optional: when editing an existing entry we pass it in.
var existing: Entry?
@State private var text = ""
var body: some View {
NavigationStack {
Form {
TextEditor(text: $text)
.frame(minHeight: 220)
}
.navigationTitle(existing == nil ? "New Entry" : "Edit")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: save)
.disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
.onAppear { text = existing?.body ?? "" }
}
}
private func save() {
if let existing {
existing.body = text
} else {
context.insert(Entry(body: text))
}
dismiss()
}
}Trace the data flow
- The user types → SwiftUI updates
@State var texton every keystroke. - The Save button's
.disabled(...)re-evaluates astextchanges, enabling itself once there's real content. - On Save, you either mutate the existing
Entryorcontext.inserta new one — SwiftData persists it. dismiss()closes the sheet. The list's@Querynotices the new object and redraws. No manual refresh anywhere.
Almost every screen in almost every app is: local @State for in-progress edits, a save that writes to persistent storage, and a query-backed list that reflects it. Once this clicks, you can read most SwiftUI apps.
Ask your assistant to "add a confirmation dialog before discarding an unsaved entry." It's a small, self-contained change — great practice at prompting for an incremental edit to existing code rather than regenerating the file.