How to Prompt a SwiftUI Screen from a Spec

SwiftUI generators love to be helpful. They invent REST fields, Sign in with Apple, navigation the spec omitted, and APIs that shipped after iOS 18. You paste the output into Xcode 16 and spend the afternoon deleting Liquid Glass, a mystery userId, and a network call sitting in the view body.
The matching generator is the SwiftUI Screen from a Spec (iOS 18) prompt. Browse related cards in the PromptDig library (Browse more prompts). When a filled run survives, share the version you actually use (Share a prompt).
Lock the API surface before you emit a view
List the SwiftUI symbols you will use. Reject anything not in iOS 18. If the spec asks for a later API, write a fallback or a refuse line. Target is iOS 18.0 unless you specified otherwise. Do not use unannounced APIs. Do not add a package the spec did not name.
@Observable is fine on 18. Form, TextField, Button, .disabled, VoiceOver labels, Preview macros: fine. Alamofire, a random image loader, and iOS 26 chrome: not fine.
Architecture is the one you named: MVVM or a simple view. Do not upgrade a simple view into a coordinator graph because the model has an email field.
Types from Models only, states not fetches in the body
Every property on a struct must appear in Models or Spec. Mark invented fields as FORBIDDEN. If JoinResponse has ok and reason, you do not get a userId. If WaitlistStatus has seatsLeft and cohortName, you do not invent a waitPosition.
Published or @Observable state maps to spec states: empty, loading, error, loaded, submitting, full. Loading is a state. Do not fetch in the view body. Inject a client. If JoinClient and the endpoint URL are not in the spec, list them under Gaps instead of minting /api/waitlist/v3.
Keep the view under about 200 lines. One file. Comments only for non-obvious accessibility. Copy comes from the spec: success, error, empty cohort. Do not rewrite it into marketing.
Accessibility, previews, and tests that are names not theater
VoiceOver labels, traits, Dynamic Type, Reduce Motion. If the spec says the button is "Join waitlist," that is the accessibility label. Success is an announcement. Error is text, not color alone. Reduce Motion: no bounce; a 1.2s dismiss can stay as a delay without a spring.
Previews use the Xcode 16 Preview macro. Light and dark. Dynamic Type XXL. Empty and error. Preview data only. If you were given seatsLeft 4, seatsLeft 0, and a network error, those are the previews. Do not invent a celebrity email.
Tests to add: five test names. Do not write fake passing tests. join_disabledWhenEmailEmpty is a name. A green test file with empty bodies is not.
Gaps: navigation, endpoints, asset catalog colors, and copy the spec omitted. Color("WaitlistAccent") must exist in the catalog if you used it. No deep link if Nav said no deep link.
Fill the card, then run
Paste the spec, the models you actually have, and the iOS floor. If an endpoint is missing, leave it missing.
Screen name and job: [Screen]
Spec (layout, states, actions): [Spec]
Data I actually have: [Models]
Navigation: [Nav]
Design tokens I may use: [Tokens]
Minimum iOS: [18.0]
Architecture: [MVVM / simple view]
Forbidden APIs: [Forbidden]
Accessibility notes: [A11y]
Preview data: [Previews]
A compiling WaitlistGate with three honest previews beats a 400-line screen that calls a URL nobody owns. When a run marks userId FORBIDDEN and refuses an iOS 26 API, share that filled card. That lock list is the reusable part.
Do not treat a successful compile as a finished screen. If the spec omitted an endpoint, the Gaps list is the handoff to whoever owns JoinClient. If Color("WaitlistAccent") is missing from the asset catalog, add it or switch to a system color the spec allowed. Do not silently substitute a random accent.
When you re-run, change only the spec fields that changed. A new empty-state string should not reopen the API lock. Keep iOS 18 as a floor until the repo's deployment target moves, and say so in the card so the model does not "helpfully" adopt a newer modifier.