Project 1 · Rubric and defend your codeswift-6.4/ios-26
Lesson 4 / 4
Unit 21 · Counter+ / Tip Splitter

Project 1 · Rubric and defend your code

Note

Unit 21 · Project 1 · Rubric. Grade your own build against this before starting Project 2. The defend-your-code prompts are the real test: shipping working code is table stakes; explaining why it is built this way is the skill.

A rubric is not a formality. On this ladder, "it works" is the floor. What separates a warm-up from a portfolio piece is whether the choices were deliberate and whether you can say why. Score yourself honestly — a red row you can explain beats a green row you got by luck.

Rubric

Area Meets bar Falls short
State Only stored what can't be derived; totals are computed Stored total/tip in @State and updated them by hand
Counter floor Clamped at 0; minus/Reset disabled at 0 Count goes negative, or button enabled but inert
Money type Decimal throughout Double anywhere in the money path
Empty state Blank subtotal shows a prompt, not $0.00 Wall of zeros, or a crash on empty input
Divide-by-zero Party clamped in the stepper and the math Relies on only one guard, or none
Formatting .currency(code:), locale-aware Manual string building, "$" + ...
Accessibility Icon buttons carry accessibilityLabels Icon-only controls VoiceOver can't name
Light/dark Correct in both; no hard-coded colors Only checked light; unreadable in dark

Accessibility is graded

Turn on VoiceOver (or the Accessibility Inspector) and tab through both screens. Every control must announce what it does. An icon-only + that reads as "plus button" fails; it should read "Increment by 2, button." The tip result should read as a label/value pair. This is not extra credit on Segue — an app a blind user can't operate is not done.

Light and dark are graded

You wrote no color literals, so both modes should be correct for free — but verify it. Toggle dark mode and look for anything you hard-coded without realizing (a white background, a black text color). If you used only semantic colors (.primary, .secondary, Color.accentColor, system materials), both modes are handled. If something breaks, that is a hard-coded value hiding somewhere.

Tip

The fastest way to catch a light/dark bug is Xcode's canvas: pin a preview in .dark and one in .light side by side with .preferredColorScheme. Anything that only works in one is a literal you need to replace with a semantic color.

Defend your code

Answer these out loud or in a comment block. If an answer is "I don't know," that is the part to revisit.

  1. Why @State and not a view model / @Observable type here? The data is owned by a single view, never shared, and doesn't outlive the view. @State is the right-sized tool; an @Observable model would be ceremony with no payoff. When does that change? (Answer: when state is shared across views or must persist — Project 2.)

  2. What happens when party size is 0? It can't reach 0 — the stepper is bounded 1...50 and perPerson divides by max(partySize, 1). Explain why you kept both guards even though either alone would do.

  3. Why is total computed rather than stored? Because it is a pure function of the inputs. Storing it introduces a second source of truth you must keep in sync; computing it makes staleness impossible.

  4. Why Decimal instead of Double for money? Walk through 0.1 + 0.2 in Double and explain what that does to a running bill total.

  5. Where does the "can't decrement below zero" rule live, and why there? In the clamp at the assignment (max(0, count - step)) and the .disabled(count == 0). Explain the division of labor: one enforces the invariant, the other communicates it.

Definition of done

  • Every rubric row is green, or you can articulate exactly why a row is amber and what you'd do with more time.
  • You answered all five defend-your-code prompts without looking at the milestone lessons.
  • VoiceOver reads both screens sensibly; both color schemes are correct.

When that holds, you have earned Project 2 — where the data finally outlives the app launch.

Knowledge check

Q: The app works perfectly but you stored total in @State and update it in each button action. Does it pass? No. It may behave correctly today, but it violates the single-source-of-truth rule the project exists to teach — the total is derivable, so it must be computed. Working code with the wrong shape doesn't meet the bar.

Q: You only tested in light mode and it looks fine. Is light/dark satisfied? No — you have to verify dark. "Looks fine in light" tells you nothing about dark, where a hard-coded white background or black text would fail. Untested is not the same as correct.