Staybnb — rubric and defend your code
Unit 27 · Staybnb rubric. Grade the behavior on a full catalog and on both form factors, then answer the defend-your-code prompts. This is the last Tier 2 project before AWS in Project 8; the architecture discipline here carries forward.
Staybnb is judged on composition holding up at scale: many listings on the map, many filters composed cleanly, one layout across iPhone and iPad, and a search that runs where it should. Grade it with a thousand listings loaded and the search field active, not with a demo set of ten.
Rubric
| Area | Meets the bar | Falls short |
|---|---|---|
| Map | Custom price pins, tap drives selection, camera follows results | Stock markers, camera fights the user, no result framing |
| Clustering | Dense map stays legible; clusters expand on zoom | Overlapping pins; clustering done after building annotations |
| Filter model | Filter value type, Equatable, pure apply(to:) |
Filter logic tangled into the view; not testable |
| Render discipline | Filter/sort in a model on change; body reads results |
Filter/sort inlined in body; runs every render |
| Identity | ForEach uses Listing.id; reorders animate correctly |
id: \.offset/\.self; rows rebuild on reorder |
| Explore & detail | Category cards with a paging gallery; a detail with host, amenities, reviews, map | A bare list; a detail missing gallery/host/reviews |
| Booking | Nights/guests drive a derived price breakdown; nothing stored | A stored total kept in sync by hand |
| Wishlist | One @Observable store; heart on card + detail + a Wishlists tab agree |
Separate saved sets per screen; hearts drift |
| Split view | One NavigationSplitView, shared selection, adapts to iPhone |
Separate iPad/iPhone code paths; panes drift out of sync |
| Edge function | Server-side filter + pricing; small payload to device | All filtering on device; pricing computed client-side |
| Security | Service-role key only in the function | Elevated key in the app bundle |
| Craft | Light/dark, Dynamic Type, VoiceOver on list and filters | Fixed sizing, unlabeled filter controls |
Mark "meets the bar" only after you have scrolled a large result set while typing and watched the frame rate hold.
Defend your code
Why is filtering a pure function on a value type instead of a method on the view?
Because Filter.apply(to:) as a pure function of (Filter, [Listing]) is unit-testable, safe
to run off the main actor, and callable once when inputs change. A view-bound filter cannot be
tested in isolation and gets re-run on every render. The value-type Filter is also Equatable,
so I can skip recomputation when it has not actually changed.
Where does your filter and sort run, and why not in body?
In the model's recompute(), driven by didSet on the filter and the listings — so it runs
once per change. body re-runs on every render, so filtering or sorting there re-scans the whole
catalog per keystroke on the main actor. body only reads the already-computed results.
Why one NavigationSplitView instead of separate iPad and iPhone layouts?
Because it collapses to a stack on iPhone automatically, so I author the layout once and it
adapts. A single shared selection binding keeps list, detail, and map in sync in any pane. Two
code paths would double the surface area and invite the panes to drift apart.
Why does search run in an edge function, and what stays on the device?
The heavy query — filtering the full catalog, computing per-stay pricing, ranking — runs
server-side where the data lives and where one authoritative pricing rule can change without an
app release. The device receives a small page and does last-mile filtering on it with the same
pure apply(to:). The service-role key never leaves the function.
How does ForEach identity affect the results list?
It uses Listing.id (stable Identifiable), so when the filter or sort reorders results,
SwiftUI diffs and moves rows, preserving cell state and animating correctly. Index-based
identity would make every reorder look like a wholesale change and force rebuilds.
Knowledge check
Q: An interviewer loads a thousand listings and types in the search field. What should hold?
The frame rate. Filtering and sorting run once per change in the model, not per render, and rows
are keyed by stable id so reorders move rather than rebuild. Nothing expensive runs in body.
Q: What is the one architectural line this project draws that carries into Project 8? Put heavy, authoritative work on the server (the edge function) and keep the device rendering already-shaped data with pure, testable transforms. That split — server owns the truth and the cost, client owns the presentation — is exactly how the AWS-tier projects are built.