How to Turn AI div Text Data into Hierarchical SwiftUI Content Sections Step by Step
By the end of this tutorial, you’ll be able to take AI-generated div text data (or similar structured content) and turn it into a clean hierarchy of SwiftUI…

By the end of this tutorial, you’ll be able to take AI-generated div text data (or similar structured content) and turn it into a clean hierarchy of SwiftUI sections: headers, body copy, notes, and footnotes.
You’ll see how this maps directly onto Uzori’s approach: constraining AI output into a server-validated schema, then rendering it as nested VStack/HStack layouts instead of a chat transcript.
Prerequisites
Before you start, make sure you have:
- Xcode 15+ with SwiftUI support
- Basic familiarity with Swift and SwiftUI (
VStack,Text,ForEach) - A sample JSON payload representing AI-generated
div-like content - Optional: an understanding of server-driven UI and Uzori’s model of server-validated schemas
In this tutorial, we’ll keep the backend abstract, but the patterns map directly to how the Uzori iOS SDK consumes server-approved layouts and streams SwiftUI screens into your app.
1. Understand the Goal: Structured Content, Not Chat
Most iOS AI experiences start as chat: you send a prompt, get a paragraph of text, and display it in a Text view.
That’s not good enough for real apps.
Instead, we want to:
- Generate structured content nodes, not plain strings
- Assign roles:
headline,subheadline,body,note,footnote,cta - Map roles to SwiftUI primitives (
Text,Button,VStack,HStack) - Enforce constraints: max lines, truncation, emphasis rules
Apple’s own primitives make this style of composition natural:
VStackandHStackare built for nested hierarchies of content (VStack docs)Textsupports styling, truncation, line limits, and emphasis rules (Text docs)
Why this matters for AI:
- LLMs are great at emitting structured JSON or HTML-like trees
- You can interpret those
divtrees as content sections - Then render them in SwiftUI as native, non-chat experiences
Common failure in this step:
Treating AI output as a single blob of text. If you don’t plan for role-based sections now, you’ll end up retrofitting structure later under deadline pressure.
2. Define a Typed Content Schema for Sections
To generate safe SwiftUI UI from AI output, you need a constrained schema instead of arbitrary view code.
We’ll define a minimal schema that fits most content-driven flows:
sectionnodes: top-level blocks like headline + bodyrole: semantic role (header, body, note, footnote, cta)children: nested content blocks
Example Swift model
enum ContentRole: String, Codable {
case header
case subheader
case body
case note
case footnote
case cta
}
struct ContentNode: Identifiable, Codable {
let id: String
let role: ContentRole
let text: String?
let children: [ContentNode]?
}
struct ContentDocument: Codable {
let sections: [ContentNode]
}
This mirrors how Uzori constrains its generative UI:
- The AI can only emit roles you define
- The backend validates each node before the client renders it
- SwiftUI simply composes the approved tree
Common failure in this step:
Letting the model emit arbitrary SwiftUI strings or untyped Any. That makes server validation nearly impossible and opens you up to layout chaos.
3. Represent AI div Text Data as JSON
AI output often arrives as HTML-like div trees, Markdown, or loosely structured JSON.
To make this usable in SwiftUI, we convert it into a JSON payload that matches our ContentDocument schema.
Example AI div-like structure (conceptual)
{
"sections": [
{
"id": "s1",
"role": "header",
"text": "Roaming protection for your next trip",
"children": []
},
{
"id": "s2",
"role": "body",
"text": "We’ve tailored a plan based on your travel dates and usage.",
"children": [
{
"id": "s2-1",
"role": "note",
"text": "You can adjust this at any time from Settings.",
"children": []
}
]
},
{
"id": "s3",
"role": "footnote",
"text": "Taxes and fees may apply.",
"children": []
}
]
}
This payload could be:
- Generated by an LLM directly (using tools / function calling)
- Derived from HTML
divnodes on your server - Produced by Uzori’s AI engine and validated before sending to iOS
Common failure in this step:
Mixing layout metadata (width, padding) with content roles. Keep layout decisions in the schema contract (e.g., stacks and modifiers) and content as text + roles. Uzori’s approach relies on this separation to keep UI predictable.
4. Decode the JSON into SwiftUI-Friendly Models
Once you have the JSON, decoding into your Swift models is straightforward.
Decoding JSON into ContentDocument
func loadContentDocument(from data: Data) throws -> ContentDocument {
let decoder = JSONDecoder()
return try decoder.decode(ContentDocument.self, from: data)
}
In a real app, this data might come from:
- Your own server-driven UI endpoint
- A Uzori endpoint that streams generative UI
- A local JSON fixture used during integration
Example usage in SwiftUI preview:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let sampleJSON = """{
\"sections\": [
{"id": "s1", "role": "header", "text": "Sample Header", "children": []},
{"id": "s2", "role": "body", "text": "Sample body.", "children": []}
]
}""".data(using: .utf8)!
let document = try! loadContentDocument(from: sampleJSON)
return ContentDocumentView(document: document)
}
}
Common failure in this step:
Ignoring decoding errors. If an AI or backend starts emitting unexpected roles or shapes, you want to fail fast on the server or at decode time, not during layout.
5. Map Roles to SwiftUI Text Styles with Constraints
Now we turn roles into actual SwiftUI Text views while enforcing constraints on length, emphasis, and line counts.
Define a renderer for a single node
struct ContentNodeView: View {
let node: ContentNode
var body: some View {
switch node.role {
case .header:
Text(node.text ?? "")
.font(.title)
.fontWeight(.bold)
.lineLimit(2) // enforce header length
.truncationMode(.tail)
case .subheader:
Text(node.text ?? "")
.font(.headline)
.lineLimit(2)
case .body:
Text(node.text ?? "")
.font(.body)
.lineLimit(6) // avoid walls of text
.truncationMode(.tail)
case .note:
Text(node.text ?? "")
.font(.footnote)
.italic()
.lineLimit(3)
case .footnote:
Text(node.text ?? "")
.font(.caption)
.lineLimit(2)
case .cta:
// We’ll handle CTA separately in Step 7
EmptyView()
}
}
}
This pattern reflects a core Uzori principle: structure over chaos.
Textis used with explicit truncation andlineLimit- Different roles get different emphasis
- No arbitrary styling from the model — only what you allow
Common failure in this step:
Letting the AI decide line breaks and emphasis with raw Markdown. You lose consistency and risk layouts that blow up on small devices.
6. Compose Div Hierarchies as Nested Stacks
SwiftUI’s compositional model maps naturally to div hierarchies. VStack is essentially the native analogue of nested vertical divs.
Render a whole document using nested stacks
struct ContentDocumentView: View {
let document: ContentDocument
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(document.sections) { section in
ContentSectionView(node: section)
}
}
.padding()
}
}
}
struct ContentSectionView: View {
let node: ContentNode
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ContentNodeView(node: node)
if let children = node.children, !children.isEmpty {
VStack(alignment: .leading, spacing: 4) {
ForEach(children) { child in
ContentSectionView(node: child)
}
}
.padding(.leading, 8) // visual hierarchy
}
}
}
}
This maps directly to the research insight that SwiftUI’s VStack and Text primitives make it practical to render deterministic, hierarchical layouts from structured content.
Common failure in this step:
Flattening the hierarchy into a single list. You lose semantic grouping and end up manually managing spacing and indentation in fragile ways.
7. Add Interactive CTAs while Keeping Safety
AI-generated content often includes actions: primary CTAs, secondary links, or toggles.
Instead of letting the model emit arbitrary buttons, we treat CTAs as another role with a constrained interaction model.
Extend the schema for CTAs
Update the model to carry metadata:
struct ActionMetadata: Codable {
let actionId: String
let label: String
}
struct ContentNode: Identifiable, Codable {
let id: String
let role: ContentRole
let text: String?
let children: [ContentNode]?
let action: ActionMetadata?
}
Now update the renderer:
struct ContentNodeView: View {
let node: ContentNode
let onAction: (ActionMetadata) -> Void
var body: some View {
switch node.role {
case .cta:
if let action = node.action {
Button(action.label) {
onAction(action)
}
.buttonStyle(.borderedProminent)
} else {
EmptyView()
}
// ... other cases from Step 5, now calling a shared helper
default:
textView
}
}
@ViewBuilder
private var textView: some View {
switch node.role {
case .header:
Text(node.text ?? "")
.font(.title)
.fontWeight(.bold)
.lineLimit(2)
case .body:
Text(node.text ?? "")
.font(.body)
.lineLimit(6)
case .note:
Text(node.text ?? "")
.font(.footnote)
.italic()
.lineLimit(3)
case .footnote:
Text(node.text ?? "")
.font(.caption)
.lineLimit(2)
default:
Text(node.text ?? "")
}
}
}
In ContentSectionView, pass a handler that routes actions to your app logic:
struct ContentSectionView: View {
let node: ContentNode
let onAction: (ActionMetadata) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ContentNodeView(node: node, onAction: onAction)
// ... children as before
}
}
}
How this aligns with Uzori:
- The AI composes which CTAs appear and where
- Your backend maps
actionIdto specific API calls or navigation - The iOS side only deals with server-validated, known action IDs
Common failure in this step:
Allowing the AI to embed URLs or raw HTTP endpoints directly in the UI schema. Always resolve actions on your server and expose them as typed IDs.
8. Enforce Length, Emphasis, and Layout Rules Centrally
To keep AI-generated UI polished and predictable, enforce constraints centrally instead of per-node.
Create a style and rules layer
struct ContentStyleRules {
static let maxHeaderLines = 2
static let maxBodyLines = 6
static let maxNoteLines = 3
static func headerText(_ text: String) -> some View {
Text(text)
.font(.title)
.fontWeight(.bold)
.lineLimit(maxHeaderLines)
.truncationMode(.tail)
}
static func bodyText(_ text: String) -> some View {
Text(text)
.font(.body)
.lineLimit(maxBodyLines)
.truncationMode(.tail)
}
static func noteText(_ text: String) -> some View {
Text(text)
.font(.footnote)
.italic()
.lineLimit(maxNoteLines)
}
}
Use these helpers in your renderer:
@ViewBuilder
private var textView: some View {
let value = node.text ?? ""
switch node.role {
case .header:
ContentStyleRules.headerText(value)
case .body:
ContentStyleRules.bodyText(value)
case .note:
ContentStyleRules.noteText(value)
case .footnote:
Text(value)
.font(.caption)
.lineLimit(2)
default:
Text(value)
}
}
This pattern echoes the research insight that layout discipline, not raw output size, is the real constraint for AI-generated SwiftUI UIs.
Common failure in this step:
Sprinkling styling logic across many views. When you eventually adjust your content guidelines, you’ll have to hunt down dozens of magic numbers.
9. Integrate with Uzori-Style Server-Driven Flows
Everything you’ve built so far can sit on top of your own server or an AI engine like Uzori.
With Uzori’s iOS SDK:
- The AI engine uses your OpenAPI spec to understand your backend
- It composes flows as structured screens, not free-form text
- Each screen is validated server-side against a schema similar to
ContentDocument - The SDK streams SwiftUI views into your app via a single integration point
You can plug in your renderer in two ways:
- As a fallback content renderer for generic content screens
- As one layer of a richer flow, alongside lists, product cards, and comparison tables
If you’re working with media overlays and more complex layouts, see the related guide: Divs for Media Layout on iOS: A Pillar Guide to Image, Video and Text Overlays.
Common failure in this step:
Treating the client as the source of truth for layout decisions. In AI-native apps, keep the intelligence and orchestration on the server, and let the iOS client focus on performant, native rendering.
10. Test, Iterate, and Guardrail Your AI Content
Finally, treat this pipeline as a product surface, not just an experiment.
Practical test tips
- Snapshot tests: Render
ContentDocumentViewwith fixtures and snapshot for regressions - Extreme inputs: Feed AI outputs with very long text, missing fields, and nested children
- Latency tests: Make sure your decoding and rendering remain smooth at 60fps
Guardrails to implement
- Reject content with unknown
rolevalues on the server - Cap children depth (e.g., max 3 levels of nesting)
- Strip unsupported emphasis (e.g., multiple CTAs in a single section)
These measures align with the broader trend: structured outputs and typed schemas are the most reliable way to bring AI into production iOS apps.
Common failure in this step:
Shipping the first working prototype into production. AI output will drift over time; guardrails and tests are what keep your UX consistent.
FAQ: Troubleshooting AI-to-SwiftUI Content Pipelines
1. How do I generate the JSON schema from an LLM?
Use function calling or tools-style APIs. Define a function that expects ContentDocument-like shapes with specific roles. The model returns JSON that your backend validates before sending to iOS.
For example, in OpenAI-style tooling you might define a tool like create_content_document with fields:
sections: array of objects withid,role,text,children
Your server rejects any extra fields or unsupported roles.
2. What if the AI returns too much text for a given section?
You have two layers of defense:
- Server-side: truncate or split content before sending the document to iOS
- Client-side: enforce
lineLimitandtruncationModeonText
SwiftUI’s Text API is designed for this: you can constrain line counts and let the system handle ellipsis and truncation.
3. How do I handle localization with AI-generated content?
You have two options:
- Generate localized content directly from the AI (one language per request)
- Use the AI only for structure and template selection, then fill in localized strings from your own catalog
In both cases, keep your schema language-agnostic: role and actionId should be stable identifiers, not user-facing strings.
4. Can I mix this approach with hand-built SwiftUI screens?
Yes. This pattern is ideal for content-heavy or assistant-like flows: onboarding, explainer screens, recommendations, configuration wizards.
For core critical flows (checkout, auth), most teams still prefer fully hand-designed screens. You can embed AI-driven sections as supporting content inside those screens.
5. How does this differ from a traditional server-driven UI framework?
Traditional SDUI gives you remote layouts but no intelligence: you still hand-author every screen.
With AI plus a constrained schema:
- The AI composes the content and flow
- The server validates against your contract
- The client renders with native SwiftUI primitives
Uzori sits exactly at this intersection: generative UI plus server-driven safety for SwiftUI apps.
By following this step-by-step pipeline, you’ve:
- Turned AI
divtext data into a typed, validated content schema - Mapped roles to structured SwiftUI sections with
VStackandText - Added interactive CTAs without sacrificing safety
- Put constraints and guardrails at the center of your AI UI architecture
This is the foundation Uzori builds on: from AI answer to SwiftUI screen, without ever falling back to yet another chat box.