What if designing a SwiftUI form was as easy as describing it in plain English?
With the power of AI and a touch of Swift, you can generate beautiful, functional iOS forms from a simple prompt — no layout code required.
Imagine building user interfaces in your iOS app without writing a single line of SwiftUI layout code. That’s now possible with the help of Generative AI. In this guide, you’ll learn how to dynamically generate SwiftUI forms from AI-generated JSON layouts — perfect for login, registration, or survey flows that may change often.
In this step-by-step guide, you’ll learn how to:
- Create form layouts from natural-language prompts
- Decode them from JSON
- Render SwiftUI views dynamically
- Bind user input and handle actions like “Register”
- Safely load your OpenAI API key from a .env file
What We’re Building
A full registration form with:
🖼 A logo image at the top
📝 Title: “Registration”
📥 Fields: Full Name, Email, Password, Confirm Password
✅ A Submit button that collects values and handles logic
All dynamically generated from AI using a simple prompt.

1. Your Prompt for the AI
let prompt = """
Generate a JSON-based SwiftUI UI layout for a user registration form with the following:
- A top logo image titled "logo"
- A heading that says "Registration"
- A text field for full name with placeholder "Enter your full name"
- A text field for email with placeholder "Enter your email"
- A secure field for password with placeholder "Enter your password"
- A secure field for confirming password with placeholder "Confirm your password"
- A submit button titled "Register" that triggers the action "register"
Use a vertical stack (VStack) with 16 spacing and 20 padding, and set the background color to white (#FFFFFF). All input fields should have 10 padding, 5 corner radius, and light gray background (#F0F0F0). The submit button should have 10 padding, 5 corner radius, a blue background (#007BFF), and white text (#FFFFFF). The heading text should use the "largeTitle" font and a text color of #000000.
Only return a valid JSON object, with no explanation or markdown formatting.
"""
This will produce a json layout like this (simplified):
{
"type": "VStack",
"children": [
{ "type": "Image", "title": "logo" },
{ "type": "Text", "value": "Registration", "font": "largeTitle" },
{ "type": "TextField", "placeholder": "...", "binding": "fullName" },
...
]
}
2. Sending the Prompt to OpenAI
Use OpenAIService.swift
to send your prompt and receive the layout:
import Foundation
class OpenAIService {
private let apiKey = Secrets.shared.get("OPENAI_API_KEY") ?? ""
private let endpoint = URL(string: "https://api.openai.com/v1/chat/completions")!
func fetchLayout(for userPrompt: String, completion: @escaping (Result<ViewLayout, Error>) -> Void) {
let fullPrompt = PromptTemplates.layoutPrompt(from: userPrompt)
let body: [String: Any] = [
"model": "gpt-4o",
"messages": [
["role": "system", "content": "You are a SwiftUI layout generator."],
["role": "user", "content": fullPrompt]
],
"temperature": 0.2
]
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
} catch {
completion(.failure(error))
return
}
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data", code: -1)))
return
}
do {
let response = try JSONDecoder().decode(OpenAIResponse.self, from: data)
if let jsonText = response.choices.first?.message.content {
let cleaned = jsonText
.replacingOccurrences(of: "```
json", with: "")
.replacingOccurrences(of: "
```", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
let layout = try JSONDecoder().decode(ViewLayout.self, from: Data(cleaned.utf8))
print(cleaned)
completion(.success(layout))
} else {
completion(.failure(NSError(domain: "Empty choice", code: -1)))
}
} catch {
completion(.failure(error))
}
}.resume()
}
}
private struct OpenAIResponse: Codable {
struct Choice: Codable {
struct Message: Codable {
let role: String
let content: String
}
let message: Message
}
let choices: [Choice]
}
This uses GPT-4o
as the model and decodes the response into ViewLayout
.
struct ViewLayout: Codable {
let type: String // e.g., "VStack"
let spacing: Int
let padding: Int
let backgroundColor: String
let children: [ViewElement]
}
struct ViewElement: Codable {
let type: String // e.g., "TextField", "Image", "Button", "Text"
let title: String? // Used for Image or Button
let value: String? // Used for static text (e.g. validation messages)
let placeholder: String? // For input fields
let binding: String? // Data key (e.g. "email", "password")
let action: String? // Button action identifier (e.g. "login")
let font: String? // e.g., "caption", "body", "title"
let cornerRadius: Int? // For styling
let padding: Int? // For styling
let backgroundColor: String? // Optional override
let foregroundColor: String? // Optional text color
let children: [ViewElement]? // For nested HStack/VStack elements
}
3. How to Get and Use Your OpenAI API Key
Step 1: Sign Up and Create a Key
- Go to https://platform.openai.com/account/api-keys
- Sign in or create an account
- Click Create new secret key
- Copy and save the key (starts with sk-...)
Step 2: Add .env File to Xcode Project
Create a plain text file in your Xcode root directory named .env:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Don't commit
.env
to GitHub! Add it to your .gitignore.
Step 3: Load Key with Secrets.swift
Your helper class:
class Secrets {
static let shared = Secrets()
private var secrets: [String: String] = [:]
init() {
if let path = Bundle.main.path(forResource: ".env", ofType: nil),
let content = try? String(contentsOfFile: path) {
let lines = content.split(separator: "\n")
for line in lines {
let parts = line.split(separator: "=", maxSplits: 1)
if parts.count == 2 {
secrets[String(parts[0])] = String(parts[1])
}
}
}
}
func get(_ key: String) -> String? {
return secrets[key]
}
}
And in OpenAIService.swift
:
private let apiKey = Secrets.shared.get("OPENAI_API_KEY") ?? ""
Done! You can now access the OpenAI API securely in your app.
4. Rendering the Layout with SwiftUI
DynamicFormView
uses ViewLayout
and ViewElement
to build real views:
struct DynamicFormView: View {
let layout: ViewLayout
@State private var formValues: [String: String] = [:]
@State private var errorMessage: String?
var body: some View {
ScrollView {
buildView(from: layout)
.padding(.horizontal)
}
}
@ViewBuilder
private func buildView(from layout: ViewLayout) -> some View {
VStack(spacing: CGFloat(layout.spacing)) {
ForEach(0..<layout.children.count, id: \.self) { index in
buildElement(layout.children[index])
}
}
.padding(CGFloat(layout.padding))
.background(Color(layout.backgroundColor))
}
......
Each element type like "TextField
", "Button
", or "Text
" is rendered using:
@ViewBuilder
func buildElement(_ element: ViewElement) -> some View {
switch element.type.lowercased() {
case "text":
if let value = element.title {
Text(value)
.font(Font.from(string: element.font))
.foregroundColor(Color(hex: element.foregroundColor))
}
case "textfield":
TextField(element.placeholder ?? "", text: binding(for: element.binding))
.padding(CGFloat(element.padding ?? 0))
.background(Color(hex: element.backgroundColor))
.cornerRadius(CGFloat(element.cornerRadius ?? 0))
case "securefield":
SecureField(element.placeholder ?? "", text: binding(for: element.binding))
.padding(CGFloat(element.padding ?? 0))
.background(Color(hex: element.backgroundColor))
.cornerRadius(CGFloat(element.cornerRadius ?? 0))
case "button":
Button(action: {
handleAction(named: element.action)
}) {
Text(element.title ?? "Submit")
.frame(maxWidth: .infinity)
.padding(CGFloat(element.padding ?? 12))
.background(Color(hex: element.backgroundColor ?? "#007BFF"))
.foregroundColor(Color(hex: element.foregroundColor ?? "#FFFFFF"))
.cornerRadius(CGFloat(element.cornerRadius ?? 8))
}
case "image":
if let title = element.title {
Image(title)
.resizable()
.scaledToFit()
.frame(height: 80)
.padding()
}
case "vstack":
if let children = element.children {
VStack(spacing: CGFloat(layout.spacing)) {
ForEach(0..<children.count, id: \.self) {
buildElement(children[$0])
}
}
}
case "hstack":
if let children = element.children {
HStack(spacing: CGFloat(layout.spacing)) {
ForEach(0..<children.count, id: \.self) {
buildElement(children[$0])
}
}
}
default:
EmptyView()
}
}
5. Dynamic Input Binding
To track what the user types into any field, we use:
@State private var formValues: [String: String] = [:]
private func binding(for key: String?) -> Binding<String> {
let key = key ?? UUID().uuidString
return Binding<String>(
get: { self.formValues[key, default: ""] },
set: { self.formValues[key] = $0 }
)
}
This lets the layout define field bindings like "email
" or "password
" from JSON, and you still capture them in SwiftUI.
6. Handling Form Actions
When a button is tapped:
{
"type": "Button",
"title": "Register",
"action": "register",
"padding": 14,
"cornerRadius": 10,
"backgroundColor": "#007BFF",
"foregroundColor": "#FFFFFF"
}
You match the "action
" field in Swift:
private func handleAction(named action: String?) {
switch action?.lowercased() {
case "register":
registerAction()
default:
print("Unhandled action")
}
}
And define the action like:
private func registerAction() {
let name = formValues["fullName"] ?? ""
let email = formValues["email"] ?? ""
let password = formValues["password"] ?? ""
let confirm = formValues["confirmPassword"] ?? ""
guard !name.isEmpty, !email.isEmpty, !password.isEmpty else {
print("All fields are required")
return
}
guard password == confirm else {
print("Passwords do not match")
return
}
print("Registration successful for \(name) <\(email)>")
}
Real-World Use Cases
Here are some practical ways to use this dynamic form generation approach in real apps:
1.User Onboarding
Render different sign-up flows depending on user role (admin, guest, merchant, etc.)
2. Feedback & Surveys
Show custom survey questions without updating the app - just change the prompt or JSON remotely.
3. Admin Panels or CMS UIs
Let non-developers generate forms using a prompt or JSON schema.
Get the Full Source Code on GitHub
You can clone and try this project today. Everything is included:
🔗 GitHub Repo:
SwiftUI-AI-FormBuilder
What's Inside:
DynamicFormView.swift
-
OpenAIService.swift
+.env
handling - Layout model structs (
ViewLayout
,ViewElement
) - Prompt templates
- Working
ContentView
with Preview + Generate button - JSON validation and logging
Feel free to fork it, test your own prompts, or customize the layout. You've now got the tools to create forms in seconds - using just your words.
Conclusion: Let AI Help You Build Smarter
In this article, you learned how to turn a simple prompt into a real SwiftUI form using OpenAI. Instead of writing every button or field by hand, you can now describe your layout in plain English - and let AI do the rest.
This is a powerful way to:
- Save time when building forms or UIs
- Make your app more flexible without rewriting code
- Learn how SwiftUI and AI can work together
Even if you're just getting started with iOS development, using this approach will help you think bigger and build faster. It's like having a smart design assistant right inside your app.
Additional Resources & References
Here are some useful links and official docs to help you understand and extend what you learned:
OpenAI API
🔗 OpenAI API Documentation
Learn how to use models like GPT-4 and GPT-4o to generate text, code, and structured JSON.
SwiftUI Basics
🔗 SwiftUI Essentials - Apple Developer
A great starting point to understand how SwiftUI works.
🔗 SwiftUI Views and Controls Reference
Browse all built-in SwiftUI views and modifiers.
💡 Enjoyed this?
👉 Check out the full version on Medium
Top comments (0)