Project 2 · Rubric and defend your code
Unit 22 · Project 2 · Rubric. Grade your build before Project 3. The defend-your-code prompts target the SwiftData machinery most people get working but can't yet explain — where the container lives, how the relationship's inverse works, why a smart view can't drift.
Project 2's real test is whether the object graph is correct and whether you understand the machinery — not just that relaunch happened to keep your data once. Score honestly.
Rubric
| Area | Meets bar | Falls short |
|---|---|---|
| Persistence | Data survives force-quit + relaunch, reliably | Works sometimes; lost on a clean relaunch |
| Relationship | @Relationship with inverse: + cascade delete; deleting a list removes its reminders |
Two unlinked properties; orphaned reminders |
| Container | .modelContainer(for: [...]) once at the scene root, both types |
Attached per-view, or a type missing |
| Query | @Query fetches + observes; counts derived, not stored |
Manual fetch/reload; a stored count that drifts |
| Smart views | Today/Scheduled/Flagged derived by filtering the store | Duplicated data or a separate "smart" store |
| Reminder fields | Due date, priority, flag, notes all persist and edit | A field that doesn't round-trip |
| Editor | @Bindable form; optional-date + relationship pickers work |
Manual copy-back; date/list edits don't save |
| Search | .searchable matches titles and notes |
No search, or title-only |
| Empty state | ContentUnavailableView, not a blank list |
Blank screen when empty |
| Accessibility | Rows announce title + done; swipe actions labeled | Icon-only, unlabeled controls |
| Light/dark | Correct in both; no color literals | Only checked light |
Accessibility is graded
VoiceOver must announce each row as "title, completed/not completed," the swipe actions must be reachable and named, and the editor's controls must be operable. Add a reminder, give it a due date, complete it, delete it — all by VoiceOver alone. An app you can't operate without sight is not done.
Light and dark are graded
Toggle dark mode and check the smart tiles, the section headers, the checkmarks, the empty
state, and the editor. You used semantic colours (.secondary, .primary, system grouped
backgrounds) and the list's own colour — all adapt — so both should be correct. Verify it;
don't assume.
The sneakiest Project 2 failure is a container attached in the wrong place, giving you two
stores. It can look like it works within a session and then lose everything on relaunch. If
persistence is flaky, audit where .modelContainer lives before touching anything else.
Defend your code
Where is
.modelContainerattached, and what breaks if it's on a child view instead of the scene? It's on theWindowGroup/scene so one store is shared through the environment. Per-view attachment creates separate contexts that don't see each other's data — inserts land in one,@Queryreads another, and data appears to vanish.How does
@Relationship(deleteRule: .cascade, inverse:)change what happens when you delete a list? The cascade deletes the list's reminders with it, so there are no orphans; theinverse:tiesReminderList.remindersandReminder.listtogether so assigning one end updates the other. Explain what would go wrong with neither.When does SwiftData save? Did you call
save()anywhere, and why or why not? It autosaves on the main run loop after changes, so interactive insert/delete/mutate persists without an explicit call. You'd only calltry modelContext.save()to force an immediate write before an out-of-band read.Why can the Today tile and the Today view never disagree? Both derive from the same query of reminders filtered by the same rule — one source of truth, no stored count. Contrast that with maintaining a separate "smart list" you'd have to keep in sync.
Why is priority stored as
priorityRaw: Intwith a computedpriority, and why is theDatePickerbound through ahasDueDatetoggle? SwiftData persists stored properties, so the enum round-trips as its rawInt;dueDateis optional butDatePickerneeds a non-optional binding, so the toggle gates the picker and sets/clears the date. Explain the tradeoff each makes.
Definition of done
- Every rubric row is green or you can explain an amber one.
- Data reliably survives a real force-quit and relaunch (test it now).
- You answered all five prompts without reopening the milestone lessons.
- VoiceOver operates the whole app; both colour schemes are correct.
Then it's on to Project 3, where the data comes from the network instead of the disk.
Knowledge check
Q: Your app kept data during testing but a fresh relaunch lost it. Most likely cause?
A .modelContainer misplacement creating a second, in-memory-only context — your session saw
the unsaved context, but nothing reached the on-device store. Fix: one container at the scene
root.
Q: You delete a list and its reminders survive as orphans in "All." What did you get wrong?
The relationship's delete rule. @Relationship(deleteRule: .cascade, inverse: \Reminder.list)
makes deleting a list remove its reminders too; without the cascade (or the inverse) the
reminders keep pointing at a gone list and leak into unfiltered views.