How to Turn div Text, Image, and Video JSON into Native SwiftUI with Uzori
By the end of this tutorial, you’ll be able to take an AI-emitted JSON payload using divtext, divimage, and divvideo blocks and render it as fully native…

By the end of this tutorial, you’ll be able to take an AI-emitted JSON payload using div_text, div_image, and div_video blocks and render it as fully native, type-safe SwiftUI screens powered by Uzori.
You’ll see how to:
- Parse AI JSON into strongly typed models
- Map each block to reusable SwiftUI components
- Validate layout rules server-side
- Stream the resulting UI into your live iOS app with Uzori’s SDK
Prerequisites
Before you start, you should have:
- Xcode 16+ and iOS 17+ target (SwiftUI ready)
- Basic familiarity with SwiftUI and Codable
- A backend that can validate and serve AI-emitted JSON
- An AI integration (e.g., an LLM) that emits JSON blocks like:
div_textdiv_imagediv_video
- Uzori SDK added to your project (via Swift Package Manager or similar)
All code examples below assume Swift 5.9+.
Step 1: Define the div JSON schema for text, image, and video
To turn AI JSON into type-safe SwiftUI, you need a clear schema. The common failure here is letting the LLM emit free-form JSON; that makes parsing brittle and unsafe.
Example AI JSON payload
Here’s a minimal example of AI-emitted JSON using div_text, div_image, and div_video blocks:
{
"screen_id": "trip_recap",
"blocks": [
{
"type": "div_text",
"id": "title",
"text": "Your trip summary",
"role": "title"
},
{
"type": "div_text",
"id": "subtitle",
"text": "Highlights from the last 7 days",
"role": "subtitle"
},
{
"type": "div_image",
"id": "hero_photo",
"url": "https://example.com/images/trip-hero.jpg",
"alt": "View over the city from a rooftop",
"aspect_ratio": "16:9"
},
{
"type": "div_video",
"id": "recap_video",
"url": "https://example.com/video/trip-recap.mp4",
"poster_url": "https://example.com/images/trip-poster.jpg",
"autoplay": false
}
]
}
Recommended block fields
For text blocks (div_text), include:
id: unique string for trackingtext: the actual contentrole: semantic role liketitle,subtitle,body,caption
For image blocks (div_image), include:
idurl: network image URLalt: alt text for accessibilityaspect_ratio: e.g.,1:1,4:3,16:9
For video blocks (div_video), include:
idurl: video URL supported byAVPlayerposter_url: optional image to show before playbackautoplay: boolean
Common failure: Missing required fields (url for images/video, text for text) leads to runtime crashes or blank UI. Make these fields non-optional in your schema and validate server-side.
Step 2: Create type-safe Swift models for each div block
Once the schema is stable, you convert it into Swift Codable models. This is where you get type-safe SwiftUI from AI JSON.
Define the block enum
Use an enum with associated values to represent each block type:
enum DivBlock: Decodable, Identifiable {
case text(DivText)
case image(DivImage)
case video(DivVideo)
var id: String {
switch self {
case .text(let model): return model.id
case .image(let model): return model.id
case .video(let model): return model.id
}
}
private enum CodingKeys: String, CodingKey {
case type
}
private enum BlockType: String, Decodable {
case div_text
case div_image
case div_video
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(BlockType.self, forKey: .type)
switch type {
case .div_text:
self = .text(try DivText(from: decoder))
case .div_image:
self = .image(try DivImage(from: decoder))
case .div_video:
self = .video(try DivVideo(from: decoder))
}
}
}
Define the individual models
struct DivText: Decodable {
let id: String
let text: String
let role: TextRole
enum TextRole: String, Decodable {
case title
case subtitle
case body
case caption
}
}
struct DivImage: Decodable {
let id: String
let url: URL
let alt: String
let aspectRatio: AspectRatio
enum AspectRatio: String, Decodable {
case oneToOne = "1:1"
case fourToThree = "4:3"
case sixteenToNine = "16:9"
}
}
struct DivVideo: Decodable {
let id: String
let url: URL
let posterURL: URL?
let autoplay: Bool
}
struct DivScreen: Decodable {
let screenID: String
let blocks: [DivBlock]
enum CodingKeys: String, CodingKey {
case screenID = "screen_id"
case blocks
}
}
Common failure: Making url a String rather than URL postpones validation too far down the stack. Decode into URL directly so malformed URLs fail at parse-time.
Step 3: Map each block type to reusable SwiftUI components
With models in place, you can build reusable SwiftUI components for AI UI. Each block type becomes a small view that you can compose in any layout.
Text block to SwiftUI Text
struct DivTextView: View {
let model: DivText
var body: some View {
switch model.role {
case .title:
Text(model.text)
.font(.title)
.fontWeight(.bold)
case .subtitle:
Text(model.text)
.font(.title3)
.foregroundStyle(.secondary)
case .body:
Text(model.text)
.font(.body)
case .caption:
Text(model.text)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
Image block to SwiftUI AsyncImage
Here you map div_image JSON to SwiftUI AsyncImage:
struct DivImageView: View {
let model: DivImage
private var aspectRatioValue: CGFloat {
switch model.aspectRatio {
case .oneToOne: return 1.0
case .fourToThree: return 4.0 / 3.0
case .sixteenToNine: return 16.0 / 9.0
}
}
var body: some View {
AsyncImage(url: model.url) { phase in
switch phase {
case .empty:
ProgressView()
.frame(maxWidth: .infinity)
.aspectRatio(aspectRatioValue, contentMode: .fit)
case .success(let image):
image
.resizable()
.scaledToFill()
.aspectRatio(aspectRatioValue, contentMode: .fit)
.accessibilityLabel(Text(model.alt))
case .failure:
Image(systemName: "photo")
.resizable()
.scaledToFit()
.aspectRatio(aspectRatioValue, contentMode: .fit)
.accessibilityLabel("Image failed to load")
@unknown default:
EmptyView()
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Video block to SwiftUI VideoPlayer (AVPlayer)
This is where div_video JSON maps to SwiftUI + AVPlayer:
import AVKit
struct DivVideoView: View {
let model: DivVideo
@State private var player: AVPlayer? = nil
var body: some View {
ZStack {
if let player {
VideoPlayer(player: player)
.onAppear {
if model.autoplay {
player.play()
}
}
} else {
if let posterURL = model.posterURL {
AsyncImage(url: posterURL) { imagePhase in
switch imagePhase {
case .success(let image):
image
.resizable()
.scaledToFill()
default:
ProgressView()
}
}
} else {
ProgressView()
}
}
}
.onAppear {
if player == nil {
player = AVPlayer(url: model.url)
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Common failure: Creating AVPlayer inside body instead of state leads to re-initialization and playback glitches. Always keep the player in @State or an observable object.
A generic renderer for all blocks
Finally, create a unified renderer:
struct DivBlockView: View {
let block: DivBlock
var body: some View {
switch block {
case .text(let model):
DivTextView(model: model)
case .image(let model):
DivImageView(model: model)
case .video(let model):
DivVideoView(model: model)
}
}
}
Step 4: Validate layout rules server-side before streaming to the app
Uzori’s approach is generative UI, server-driven safety: AI composes blocks, your server validates them, and only safe layouts reach the client.
If you let the client be the first validator, you’ll get inconsistent behavior and hard-to-debug crashes. Instead, follow a server-side layout rule validation pattern similar to DivKit’s schema enforcement.
Example server-side validation rules
On your backend, validate that:
- Every
div_imagehas a validurlandalt - Every
div_videohas a validurland supported format - Layout sequences make sense (e.g.,
div_textwith roletitleappears before media blocks) - No overlapping blocks violate your design rules
For example, you might enforce:
- Rule 1: A screen must start with either a
titletext or a hero media block - Rule 2: At most one
div_videoper screen above-the-fold - Rule 3:
aspect_ratiomust be in a known set (1:1,4:3,16:9)
If validation fails, the server:
- Rejects the layout
- Optionally asks the AI to regenerate
- Returns an error or fallback layout to the client
This keeps client code simple and ensures iOS engineers don’t have to defensively code for every malformed payload.
For more complex overlay media and stacked compositions, see the companion guide: div-on-image layouts for native SwiftUI overlays.
Step 5: Stream validated SwiftUI screens into your app with Uzori
Once the server has validated the layout, Uzori’s SDK lets you stream SwiftUI screens in real time into a single integration point.
A common failure here is tightly coupling navigation and state to a specific screen structure. With Uzori, you treat the generated UI as a server-driven surface: one container view that reacts to streaming updates.
High-level Uzori integration flow
- User interacts with your app (e.g., taps “AI concierge”).
- Your app sends a request to Uzori, including:
- User input
- Context (user ID, session state)
- Backend API descriptions (often OpenAPI)
- Uzori’s engine calls your backend and/or LLM.
- Backend returns validated div JSON for the next screen.
- Uzori SDK streams block updates to your app.
- Your SwiftUI container renders
[DivBlock]usingDivBlockView.
Example SwiftUI container
This simplified example shows how you might render a full AI-driven screen:
struct UzoriScreenContainer: View {
@State private var currentScreen: DivScreen?
var body: some View {
Group {
if let screen = currentScreen {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
ForEach(screen.blocks) { block in
DivBlockView(block: block)
}
}
.padding()
}
.navigationTitle(screen.screenID)
} else {
ProgressView("Loading interface…")
}
}
.task {
await loadInitialScreen()
}
}
func loadInitialScreen() async {
// Pseudocode: fetch from Uzori engine endpoint
// let response = try await uzoriClient.fetchScreen(for: "trip_recap")
// self.currentScreen = response
}
}
In a real app, the uzoriClient would:
- Open a streaming connection (e.g., SSE or WebSocket)
- Decode incoming JSON into
DivScreen - Update
currentScreenas blocks arrive
Because each block is already mapped to a reusable SwiftUI component, streaming updates become trivial: you’re just re-rendering typed models.
Step 6: Combine Uzori with your existing backend and AI models
Uzori is designed as an AI interface layer on top of your current stack. You don’t rebuild your backend; you describe it.
To integrate:
- Describe your APIs with OpenAPI or similar.
- Expose those schemas to Uzori so the AI can:
- Understand what data it can fetch
- Choose appropriate endpoints for each task
- Teach the AI your block schema:
- Provide examples of
div_text,div_image,div_videoblocks - Show valid and invalid layouts
- Provide examples of
- Implement server validators that:
- Parse AI JSON into typed models
- Enforce layout rules
- Return only approved screens to Uzori
Once this pipeline is in place, you can:
- Generate AI concierges that feel like your app, not a generic chatbot
- Build product explorers that use carousels, forms, and media blocks
- Compress weeks of UI iteration into a streaming interface that designs itself
If you want to see this pattern end-to-end, including SDK wiring and more complex flows, the examples on Uzori’s website show how teams use div blocks plus server validation to ship AI-native UI.
FAQ: Troubleshooting div JSON to SwiftUI with Uzori
1. What happens if the AI emits an unknown block type?
If your DivBlock enum doesn’t recognize a type, decoding will throw. The safest pattern is:
- Reject unknown types on the server
- Return an error or ask the AI to regenerate
- Only send screens with known, mapped block types (
div_text,div_image,div_video) to the client
You can also add a unknown case that logs and renders a placeholder, but for production flows, server-side rejection is safer.
2. How do I handle missing image or video URLs?
Do not treat them as optional on the client. Instead:
- Make
urlnon-optional (URL) in your models - Fail JSON decoding if
urlis missing or invalid - Add server-side validation similar to DivKit: missing required fields = validation error
On the client, you can still guard against network failures, but structural problems should never reach SwiftUI.
3. Will streaming updates cause layout jank in SwiftUI?
If you:
- Keep your blocks in a stable array (
Identifiable) - Use
ForEachoverblocks - Avoid heavy recomputation in
body
then streaming updates will be smooth. Most jank comes from:
- Rebuilding
AVPlayerrepeatedly - Delaying layout until long-running tasks complete
Use @State or observable objects for heavy resources (players, decoders) and keep your block views lightweight.
4. How do I support more complex layouts like text over image?
For overlay-heavy layouts (text buttons on images, badges, gradient overlays), extend your schema with layout hints, for example:
overlay_text_block_idbadge_textgradient_style
Then map those hints into composed SwiftUI views using ZStack and overlay. For a deep dive on these patterns, see the related guide on div-on-image layouts translating overlay-heavy media UI to native SwiftUI.
5. How does this differ from traditional server-driven UI like DivKit?
Traditional SDUI frameworks like DivKit:
- Focus on server-authored templates
- Use JSON schemas with strong validation
Uzori combines that safety with AI:
- AI composes the blocks and flows
- Your server validates the JSON layout
- Uzori streams the resulting SwiftUI screens
You get AI-native experiences with the same structural guarantees as mature SDUI.
By following these steps—defining a clear div JSON schema, mapping to type-safe Swift models, building reusable SwiftUI components, validating layouts server-side, and streaming them with Uzori—you can turn any LLM response into a real, shippable native iOS interface instead of just another chat transcript.