Let's think about it this way for a second
A real app is split into layers — the View layer (SwiftUI screens), the ViewModel layer (business logic, ObservableObject), and the Data layer (SwiftData model). A Todo app needs to wire up all the CRUD operations (adding a new todo — Create, viewing the list — Read, toggling done/undone — Update, deleting an item — Delete) across all three layers.
Let's connect this to a real-world scenario
Build the three layers one at a time: `TodoItem` (@Model, data layer) → `TodoViewModel` (ObservableObject, with add/toggle/delete functions) → `TodoListScreen`/`AddTodoScreen` (View layer, List + NavigationStack). Use NavigationStack to connect the list screen to the add screen, and use `List`'s built-in swipe-to-delete for the delete operation.
Let's walk through it together
struct TodoListScreen: View {
@Query private var todos: [TodoItem]
@Environment(\.modelContext) private var modelContext
@State private var showingAddScreen = false
var body: some View {
NavigationStack {
List {
ForEach(todos) { todo in
HStack {
Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
.onTapGesture { todo.isDone.toggle() }
Text(todo.title)
}
}
.onDelete { indices in
for index in indices { modelContext.delete(todos[index]) }
}
}
.navigationTitle("Todos")
.toolbar {
Button("Add") { showingAddScreen = true }
}
}
}
}Run the Todo app and you should be able to add todos, toggle done/undone, swipe to delete, and see your data still there after restarting the app.5-minute try-it
Build the entire Todo app yourself with all three layers (Data/ViewModel/View) — verify that all four CRUD operations actually work on the Simulator.
A quick word of caution
If you delete a todo item permanently without offering an 'undo' option, that hurts the user experience — consider adding an alert dialog or an undo gesture instead.