How to Stream AI‑Generated Div Image Grids into a SwiftUI Collection View
By the end of this tutorial, you’ll have a SwiftUI screen that streams AI‑generated "div" image data from Uzori into a responsive, native grid — updated in…

By the end of this tutorial, you’ll have a SwiftUI screen that streams AI‑generated "div" image data from Uzori into a responsive, native grid — updated in real time, validated on your server, and composed from reusable SwiftUI components.
You’ll see how to:
- Integrate the Uzori SDK into an iOS app
- Validate AI‑generated layout JSON server‑side using a schema
- Map div metadata (image, text, overlays) into SwiftUI grid cells
- Stream updates from an AI agent into a live LazyVGrid
Prerequisites
Before you start, make sure you have:
- Xcode 15+ and iOS 17+ target (SwiftUI
GridorLazyVGrid) - An existing SwiftUI app (or a new project from the Xcode template)
- A backend that can:
- Expose APIs via OpenAPI (or structured JSON)
- Call an LLM that supports structured outputs (e.g., OpenAI Responses API)
- A JSON Schema for your "div image grid" layout (we’ll sketch one)
- Uzori iOS SDK credentials (or sandbox keys) and access to its engine
If you’re new to div‑style media layouts, the companion deep‑dive — Divs for Media Layout on iOS: A Pillar Guide to Image, Video and Text Overlays — walks through more complex combinations of images, video, and text overlays.
Step 1: Define a JSON Schema for Your Div Image Grid Layout
To safely stream AI‑generated UI, you need a strict contract. In this pattern, the contract is a JSON Schema describing a collection of "div image" cards.
What you’ll do in this step
- Declare a schema that your LLM must follow
- Give the AI just enough freedom to vary content, not structure
- Prepare for server‑side layout validation
Example JSON Schema
Here’s a simplified JSON Schema for a grid of image cards:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "DivImageGrid",
"type": "object",
"properties": {
"columns": { "type": "integer", "minimum": 1, "maximum": 4 },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"imageUrl": { "type": "string", "format": "uri" },
"title": { "type": "string" },
"subtitle": { "type": "string" },
"overlayText": { "type": "string" },
"aspectRatio": { "type": "number", "minimum": 0.5, "maximum": 2.0 }
},
"required": ["id", "imageUrl", "title"],
"additionalProperties": false
},
"minItems": 1,
"maxItems": 100
}
},
"required": ["columns", "items"],
"additionalProperties": false
}
You’ll use this schema in your backend when calling the LLM.
Why this matters
- OpenAI’s Structured Outputs guarantee that responses adhere to your schema, reducing retries and malformed data.
- JSON Schema is widely supported and designed for server‑side validation.
- JSONSchemaBench (10k real‑world schemas in a 2025 benchmark) shows schema‑constrained generation is now a mainstream practice.
Common failure in this step
Problem: The schema is too loose (e.g., additionalProperties: true everywhere), so the AI returns fields your app can’t handle.
Fix: Lock down optional vs required fields and disallow unexpected properties. Keep the schema narrow enough that your SwiftUI views can be strictly typed.
Step 2: Use Structured Outputs to Generate Validated Div JSON Server‑Side
Now you’ll connect your backend to an LLM with structured outputs, validate the result, and expose it as an API that Uzori can orchestrate.
What you’ll do in this step
- Call the LLM with a structured output request
- Validate the response against the JSON Schema
- Return only valid layout JSON to your iOS app
Example backend flow (pseudo‑code)
// Node / TypeScript pseudo-code using OpenAI Responses API
import { openai } from "openai";
import Ajv from "ajv";
import divImageGridSchema from "./schemas/div-image-grid.json";
const ajv = new Ajv();
const validateGrid = ajv.compile(divImageGridSchema);
export async function getDivImageGridForQuery(userQuery: string) {
const response = await openai.responses.create({
model: "gpt-4.1",
input: `Generate a grid of gowns based on: ${userQuery}`,
response_format: {
type: "json_schema",
json_schema: {
name: "DivImageGrid",
schema: divImageGridSchema,
strict: true
}
}
});
const layout = response.output[0].content[0].json; // structured output
if (!validateGrid(layout)) {
throw new Error("Invalid layout JSON");
}
return layout;
}
Expose this via a REST endpoint like GET /ai/div-image-grid?query=....
Why this matters
- OpenAI states that structured outputs ensure responses always adhere to the supplied JSON Schema, which is ideal for UI contracts.
- Server‑side validation keeps unsafe or malformed layouts from ever reaching the app.
Common failure in this step
Problem: You try to validate on the client, or skip validation entirely.
Fix: Always validate server‑side and return a single, typed structure (e.g., DivImageGrid) to your iOS app. If validation fails, return a safe fallback layout or an error that Uzori can turn into a native error screen.
Step 3: Integrate the Uzori iOS SDK as a Single SwiftUI Screen
With a validated layout endpoint in place, you can connect Uzori’s SDK to stream generative UI into your SwiftUI app. Uzori acts as the AI interface layer — orchestrating calls to your backend and returning SwiftUI screens.
What you’ll do in this step
- Add the Uzori SDK to your project
- Create a single host screen that talks to Uzori
- Prepare a container for the div image grid view
Install the Uzori SDK
Use Swift Package Manager in Xcode:
- Go to File → Add Packages…
- Enter the Uzori SDK URL (provided in your developer onboarding)
- Add the package to your app target
Create a host view
Uzori exposes a SwiftUI‑friendly API that you can compose inside your existing navigation stack. Conceptually, your host view looks like this:
import SwiftUI
import Uzori
struct AiDivGridScreen: View {
@StateObject private var uzoriSession = UzoriSession()
var body: some View {
UzoriHostView(session: uzoriSession) { state in
switch state {
case .idle:
Text("Ask the AI for products or media.")
case .loading:
ProgressView("Loading grid…")
case .render(let screen):
screen // Uzori returns a SwiftUI view
case .error(let error):
Text("Error: \(error.localizedDescription)")
}
}
.onAppear {
uzoriSession.start(flow: "div-image-grid")
}
}
}
Uzori’s engine then:
- Receives user input (e.g., a query for gowns, travel plans, etc.)
- Calls your backend (which uses structured outputs to talk to the LLM)
- Streams native SwiftUI screens back, validated by your server
Common failure in this step
Problem: Treating Uzori as a generic chat UI instead of a full-screen interface.
Fix: Integrate Uzori as a dedicated SwiftUI screen (or tab), not a message bubble. Uzori is meant to stream full native screens and flows, including your div image grids.
Step 4: Define a SwiftUI Model and Grid View for Div Image Items
Next, you’ll map the validated JSON from your backend into strongly typed Swift models and a reusable grid view.
What you’ll do in this step
- Create Swift structs matching the JSON schema
- Build a
LazyVGridto render image cards - Design a reusable
DivImageCardcomponent
Swift models for the layout
struct DivImageGridLayout: Decodable {
let columns: Int
let items: [DivImageItem]
}
struct DivImageItem: Identifiable, Decodable {
let id: String
let imageUrl: URL
let title: String
let subtitle: String?
let overlayText: String?
let aspectRatio: Double?
}
This matches the JSON Schema from Step 1.
Div image card component
struct DivImageCard: View {
let item: DivImageItem
var body: some View {
ZStack(alignment: .bottomLeading) {
AsyncImage(url: item.imageUrl) { phase in
switch phase {
case .empty:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
Image(systemName: "photo")
.resizable()
.scaledToFit()
.padding()
@unknown default:
EmptyView()
}
}
.aspectRatio(item.aspectRatio ?? 1.0, contentMode: .fit)
.clipped()
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.font(.headline)
.lineLimit(1)
if let subtitle = item.subtitle {
Text(subtitle)
.font(.subheadline)
.lineLimit(2)
}
if let overlay = item.overlayText {
Text(overlay)
.font(.caption)
.padding(.top, 2)
}
}
.padding(8)
.background(.thinMaterial)
}
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
Grid view using LazyVGrid
struct DivImageGridView: View {
let layout: DivImageGridLayout
var body: some View {
let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: layout.columns)
ScrollView {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(layout.items) { item in
DivImageCard(item: item)
}
}
.padding()
}
}
}
SwiftUI’s LazyVGrid (available from iOS 14+) gives you a responsive, efficient grid that fits native patterns.
Common failure in this step
Problem: Hard‑coding column counts or ignoring layout.columns, which makes the AI‑generated grid less adaptive.
Fix: Always drive the grid’s column configuration from the validated layout JSON so the AI can adapt density for phones vs. tablets, or different content types.
Step 5: Wire Uzori’s Generated Screens to Your Div Image Grid View
Uzori returns native SwiftUI screens composed from your schema. In this step, you’ll ensure that when Uzori streams a "div image grid" screen, it uses DivImageGridView with the server‑validated layout.
What you’ll do in this step
- Register a screen type with Uzori for div image grids
- Decode the layout JSON into your Swift models
- Render
DivImageGridViewwhen that screen type appears
Example screen registration (conceptual)
Uzori lets you map server‑approved schemas to SwiftUI views:
extension UzoriScreenRegistry {
static func registerDivImageGrid() {
Uzori.registerScreen(type: "div-image-grid") { payload in
// `payload.data` is already validated JSON from your backend
let layout = try JSONDecoder().decode(DivImageGridLayout.self, from: payload.data)
return AnyView(DivImageGridView(layout: layout))
}
}
}
Call this early in app startup:
@main
struct AiGridApp: App {
init() {
UzoriScreenRegistry.registerDivImageGrid()
}
var body: some Scene {
WindowGroup {
AiDivGridScreen()
}
}
}
Now, when the AI engine decides the right response for a user query is a grid of media cards, it:
- Calls your backend
- Gets structured layout JSON from the LLM
- Validates it server‑side
- Streams a "div-image-grid" screen to the app
- Uzori instantiates
DivImageGridView(layout:)using your native components
Common failure in this step
Problem: Treating Uzori’s payload as free‑form JSON and decoding it ad hoc per screen.
Fix: Define a stable schema per screen type (like DivImageGridLayout) and register a single renderer for it. This keeps your interface layer predictable and easier to test.
Step 6: Stream Real‑Time Updates into the SwiftUI Grid
The real power of this pattern comes when the grid updates live as the AI refines results — for example, as a user narrows filters or adds constraints.
What you’ll do in this step
- Handle streaming layout updates from Uzori
- Animate changes in the grid for a smooth UX
- Consider performance and safety constraints
Handling streaming updates
Uzori’s session exposes state changes as new screens. You can update the grid layout whenever a new payload arrives.
struct StreamingDivGridContainer: View {
@StateObject private var uzoriSession = UzoriSession()
@State private var currentLayout: DivImageGridLayout?
var body: some View {
Group {
if let layout = currentLayout {
DivImageGridView(layout: layout)
} else {
Text("Start a query to see the grid.")
}
}
.onAppear {
uzoriSession.start(flow: "div-image-grid")
}
.onReceive(uzoriSession.screenPublisher) { screen in
if screen.type == "div-image-grid" {
if let data = screen.payload.data {
if let layout = try? JSONDecoder().decode(DivImageGridLayout.self, from: data) {
withAnimation(.spring()) {
currentLayout = layout
}
}
}
}
}
}
}
Performance considerations
LazyVGridensures only visible cells are rendered, which matters whenitemscan be large.- Use cached image loading (e.g.,
AsyncImagewith URLCache or a custom cache) to avoid re‑fetching images on every minor layout change. - Keep overlays lightweight; SwiftUI’s native layout engine is optimized for simple stacks and grids.
Common failure in this step
Problem: Recreating the entire screen hierarchy on every token of AI output, causing jank.
Fix: Stream at the level of layout updates (complete DivImageGridLayout objects), not individual cells. Let Uzori batch changes and deliver coherent states that SwiftUI can animate between.
Why This Pattern Fits the 2026 AI Mobile Landscape
A few data points make this approach — streaming AI‑generated div grids into SwiftUI — especially timely:
- Sensor Tower reports ~1.7 billion global downloads of generative AI apps and ~$1.9B IAP revenue in H1 2025, with downloads up 67% half‑over‑half.
- AI app revenue (all platforms) reached $18.5B in 2025, up 180% year‑over‑year, with forecasts above $80B over the next half‑decade.
- Native UX is a differentiator: SwiftUI offers grid primitives (
LazyVGrid,Grid) and efficient layout behavior that users expect from modern iOS apps. - Structured generation and JSON Schema validation are now standard practice, making server‑driven, AI‑composed UI both safe and tractable.
In other words, the market is ready for AI interfaces that build themselves — but still feel like your app.
FAQ: Troubleshooting AI‑Generated Div Grids in SwiftUI
1. Why am I seeing blank cells or missing images in the grid?
Blank cells usually mean one of:
imageUrlis invalid or not a full URL- Network errors while loading images
- Aspect ratio constraints causing very small frames
Confirm that:
- The server validation enforces a URI format for
imageUrl - Your image host supports HTTPS and iOS app transport security
aspectRatiofields are within the allowed schema range (e.g., 0.5–2.0)
Use a fallback image (as in DivImageCard) for failed loads.
2. How do I avoid layout breaks when the AI changes columns?
Layout breaks typically arise when the AI switches between very different column counts.
Mitigations:
- Clamp
columnsin the schema (e.g., 1–4) and stick to that - Animate changes with
withAnimation(.spring())so the grid transitions smoothly - Optionally, ignore column changes below a certain threshold and let your own adaptive logic decide based on device size class
3. Can I mix text‑only divs and image divs in the same grid?
Yes, but you should:
- Extend your JSON Schema with a
kindfield ("image","text", etc.) - Create multiple SwiftUI components (e.g.,
DivImageCard,DivTextCard) - Switch on
kindwhen building the grid cells
This pattern is covered in more depth in the related guide on divs for media layout: Divs for Media Layout on iOS: A Pillar Guide to Image, Video and Text Overlays.
4. How does Uzori compare to DivKit or web‑first generative UI tools?
For iOS teams:
- Uzori: Streams full SwiftUI screens, combining generative UI with server‑driven safety and native performance.
- DivKit: Pure server‑driven UI; templates and JSON cards, but layout is not AI‑composed by default.
- CopilotKit, Thesys: Strong generative UI models, but primarily web‑first and less focused on SwiftUI.
If your app is native iOS and you care about SwiftUI ergonomics, Uzori gives you an AI interface layer that fits directly into your stack.
5. What’s the best way to test safety for AI‑generated grids?
Apply multiple layers:
- Schema validation on the backend (JSON Schema + Structured Outputs)
- Business rules (e.g., only allow approved domains in
imageUrl) - Rate limiting and logging for layout generation endpoints
- Client‑side assertions in debug builds to catch unexpected states
Run integration tests with fixture JSON layouts that cover edge cases (large item counts, extreme aspect ratios, missing optional fields).
Conclusion: From AI Answer to SwiftUI Grid, Safely
By combining Uzori’s SwiftUI SDK, server‑driven UI patterns, and schema‑constrained AI generation, you can:
- Turn LLM intent into live, native div image grids
- Keep layout logic on the server, under your control
- Iterate quickly on new AI flows without rewriting client code
With just one SwiftUI screen integrated, Uzori can orchestrate infinite variations of image grids, explorers, and media overlays — all streamed into your app as safe, native interfaces.
If you’re ready to move beyond chat boxes and give users AI experiences that feel like your app, not someone else’s chatbot, this div image grid pattern is a practical place to start.