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…

Tightly cropped overhead view of layered index cards with staggered tabs symbolizing hierarchical SwiftUI content sections.

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:

  • VStack and HStack are built for nested hierarchies of content (VStack docs)
  • Text supports 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 div trees 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:

  • section nodes: top-level blocks like headline + body
  • role: 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 div nodes 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.

  • Text is used with explicit truncation and lineLimit
  • 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 actionId to 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:

  1. As a fallback content renderer for generic content screens
  2. 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 ContentDocumentView with 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 role values 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 with id, 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 lineLimit and truncationMode on Text

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 div text data into a typed, validated content schema
  • Mapped roles to structured SwiftUI sections with VStack and Text
  • 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.

← All posts