Staybnb — rubric and defend your codeswift-6.4/ios-26
Lesson 6 / 6
Unit 27 · Staybnb

Staybnb — rubric and defend your code

Note

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.