💻 Coding

SwiftUI Screen from a Spec (iOS 18)

Turn a screen spec into SwiftUI locked to iOS 18 APIs, with accessibility and previews. No invented endpoints or post-iOS 18 APIs.

0.0
0Reviews
P
August 25, 2026

Prompt

Act as an iOS engineer writing SwiftUI for iOS 18 (Xcode 16). You implement only what the spec states. You do not invent REST fields, navigation the spec omitted, or APIs that shipped after iOS 18.

Inputs:
- 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 unless specified]
- Architecture: [MVVM / simple view]
- Forbidden APIs: [Forbidden]
- Accessibility notes: [A11y]
- Preview data: [Previews]

Generate:
1. API lock: list SwiftUI symbols you will use. Reject anything not in iOS 18. If Spec asks for a later API, write a fallback or a refuse line.
2. Types: structs you need. Every property must appear in Models or Spec. Mark invented fields as FORBIDDEN.
3. View code: one file. iOS 18 SwiftUI. Comments only for non-obvious a11y.
4. View model (if MVVM): published state that maps to Spec states (empty, loading, error, loaded).
5. Accessibility: VoiceOver labels, traits, Dynamic Type, Reduce Motion.
6. Previews: Xcode 16 Preview macro. Light/dark, Dynamic Type XXL, empty and error. Use Preview data only.
7. Tests to add: 5 test names. Do not write fake passing tests.
8. Gaps: navigation, endpoints, and copy the spec omitted.

Constraints:
- Target iOS 18.0. Do not use Liquid Glass, iOS 26, or unannounced APIs.
- Do not add a new package unless Spec names it.
- Do not fetch in the view body. Loading is a state.
- Keep the view under ~200 lines.

Instructions

Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.

Examples

Example Input

Screen: WaitlistGate, join a cohort waitlist
Spec: email field, Join button disabled when email empty or submitting. Success: "You're on the list for October." Error: "Could not join. Try again." Empty cohort: "No seats left in this cohort." No social login. Keyboard email. Submit on return.
Models: WaitlistStatus { seatsLeft: Int, cohortName: String }. JoinRequest { email: String }. JoinResponse { ok: Bool, reason: String } reason in {joined, full, invalid, network}. No user id.
Nav: sheet from Home. Dismiss on success after 1.2s. No deep link.
Tokens: system fonts; Color("WaitlistAccent") in asset catalog; padding 16
Min iOS: 18.0
Architecture: MVVM
Forbidden: third-party networking, iOS 26 APIs
A11y: button label "Join waitlist"; success is an announcement; do not hide error in color alone
Previews: seatsLeft 4 cohort "October"; seatsLeft 0; network error

Example Output

1. API lock (iOS 18 / Xcode 16)
SwiftUI: View, Form, TextField, Button, .disabled, .accessibilityLabel, .accessibilityAnnouncement (or AccessibilityNotification.Announcement), .onSubmit. Preview: #Preview. @Observable is fine on 18. Reject: Liquid Glass, unannounced APIs, Alamofire.

2. Types from Models only
WaitlistStatus { seatsLeft: Int; cohortName: String }
JoinRequest { email: String }
JoinResponse { ok: Bool; reason: String } // joined|full|invalid|network
No userId. FORBIDDEN.

3-4. View + VM sketch
@Observable final class WaitlistGateModel {
  var email = ""
  var submitting = false
  var banner: Banner?
  var status: WaitlistStatus
  var canSubmit: Bool { !email.isEmpty && !submitting && status.seatsLeft > 0 }
  func join() async { /* inject JoinClient; no URLSession in the view */ }
}
struct WaitlistGateView: View {
  @Bindable var model: WaitlistGateModel
  @Environment(\.dismiss) var dismiss
  var body: some View {
    Form {
      TextField("Email", text: $model.email)
        .textContentType(.emailAddress)
        .keyboardType(.emailAddress)
      Button("Join waitlist") { Task { await model.join() } }
        .disabled(!model.canSubmit)
        .accessibilityLabel("Join waitlist")
      if model.status.seatsLeft == 0 { Text("No seats left in this cohort.") }
      if let banner = model.banner { Text(banner.text) }
    }
  }
}
Success copy: "You're on the list for October." Error: "Could not join. Try again."

5. A11y
Join waitlist as the button name. Announce success string. Error is text, not red-only. Dynamic Type via Form and system fonts. Reduce Motion: no bounce; 1.2s dismiss stays.

6. Previews
#Preview("seats") { WaitlistGateView(model: .init(status: .init(seatsLeft: 4, cohortName: "October"))) }
#Preview("full") { seatsLeft 0 }
#Preview("network") { network error banner }
#Preview("XXL dark") { sizeCategory accessibilityXXXL, dark }

7. Tests (names only)
join_disabledWhenEmailEmpty
join_disabledWhenSubmitting
join_reasonFull_showsNoSeatsCopy
join_reasonJoined_announcesAndDismisses
join_reasonNetwork_showsTryAgain_notColorOnly

8. Gaps
JoinClient not in spec. No endpoint URL. Color("WaitlistAccent") must exist. No Sign in with Apple. No deep link.

Reviews (0)

Please login to leave a review.
Loading reviews...