DEV Community

Cover image for Design SwiftUI Forms with Open AI: A Beginner’s Guide to Dynamic iOS Interfaces
himalidev
himalidev

Posted on • Originally published at Medium

Design SwiftUI Forms with Open AI: A Beginner’s Guide to Dynamic iOS Interfaces

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.

From prompt to SwiftUI form — watch how a single AI-generated JSON layout creates a complete registration screen in seconds.

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.
"""
Enter fullscreen mode Exit fullscreen mode

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" },
    ...
  ]
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode


Xcode Preview of ContentView with a ‘Generate UI’ button — triggering OpenAI to create the form layout dynamically.

3. How to Get and Use Your OpenAI API Key

Step 1: Sign Up and Create a Key


OpenAI dashboard with API key generation

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
Enter fullscreen mode Exit fullscreen mode

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]
    }
}
Enter fullscreen mode Exit fullscreen mode

And in OpenAIService.swift:

private let apiKey = Secrets.shared.get("OPENAI_API_KEY") ?? ""
Enter fullscreen mode Exit fullscreen mode

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))
    }

  ......
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
Enter fullscreen mode Exit fullscreen mode

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 }
        )
    }
Enter fullscreen mode Exit fullscreen mode

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"
    }
Enter fullscreen mode Exit fullscreen mode

You match the "action" field in Swift:

private func handleAction(named action: String?) {
    switch action?.lowercased() {
        case "register":
            registerAction()
        default:
            print("Unhandled action")
    }
}
Enter fullscreen mode Exit fullscreen mode

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)>")
}
Enter fullscreen mode Exit fullscreen mode


The final SwiftUI registration form


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)