How to Implement Div-on-Image Overlays in SwiftUI with Uzori’s Generative Media Layout
By the end of this tutorial, you’ll have a working SwiftUI “div-on-image” layout where an AI agent (via Uzori) can safely place labels, badges, and CTAs over a…

By the end of this tutorial, you’ll have a working SwiftUI “div-on-image” layout where an AI agent (via Uzori) can safely place labels, badges, and CTAs over a background image—using a schema that’s validated on your server before anything renders.
You’ll learn how to:
- Define a JSON-schema-style model for overlay "divs"
- Validate that schema on your server before SwiftUI renders it
- Hook Uzori into your iOS app so an AI agent can generate these overlays
- Render multiple overlays over an image using SwiftUI’s native
overlaysystem
This is a practical, production-ready pattern: AI proposes, your server validates, SwiftUI renders.
Prerequisites
Before you start, you should have:
- Xcode 15+ with Swift 5.9+ and SwiftUI
- An existing iOS app target (iOS 17 recommended)
- Basic SwiftUI knowledge (stacks,
overlay,ZStack) - A backend service where you can host a simple API (e.g., Node, Rails, Go, etc.)
- Access to Uzori’s iOS SDK and backend (or a sandbox key)
If you want a broader conceptual overview of div overlays for media on iOS, see this related guide: Divs for Media Layout on iOS: A Pillar Guide to Image, Video and Text Overlays.
1. Understand the div-on-image Overlay Pattern in SwiftUI
SwiftUI’s overlay modifier is Apple’s native answer to “div-on-image” layouts.
From Apple’s docs:
- Multiple overlay views are collected into an implicit
ZStack. - Alignment controls the placement of each overlay.
This makes it a natural fit for:
- Badges (top-right)
- Title labels (bottom-left)
- CTA buttons (bottom-right)
- Any combination of stacked views on top of a hero image
Concrete example – a static, non-AI overlay:
struct StaticOverlayCard: View {
var body: some View {
Image("hero")
.resizable()
.scaledToFill()
.frame(height: 240)
.clipped()
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: 8) {
Text("Weekend Escape")
.font(.headline)
.bold()
Text("AI-generated itinerary")
.font(.subheadline)
}
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
}
.overlay(alignment: .topTrailing) {
Text("New")
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.ultraThinMaterial)
.clipShape(Capsule())
.padding()
}
}
}
This pattern is what we’ll let Uzori and your AI agent generate dynamically.
Common failure at this step:
- Trying to manually layer everything in a single
ZStackbefore you understandoverlay. Start with directoverlayusage; then we’ll generative-ify it with Uzori.
2. Design a Schema for AI-Generated Overlays
To make AI-generated UI safe, you need a schema-first approach.
JSON Schema and OpenAPI are industry standards for describing structure. They map perfectly to an AI UI pipeline: the model proposes a layout, your server validates it, the app renders only what passes validation.
We’ll define an OverlayDiv schema that describes:
- Type –
label,badge,cta - Alignment – e.g.,
topLeading,bottomTrailing - Relative position – optional x/y offsets (0–1) over the image
- Content – text, optional action ID
Swift model representation:
struct OverlayDiv: Codable, Identifiable {
enum Kind: String, Codable {
case label
case badge
case cta
}
enum Alignment: String, Codable {
case topLeading
case top
case topTrailing
case leading
case center
case trailing
case bottomLeading
case bottom
case bottomTrailing
}
let id: String
let kind: Kind
let alignment: Alignment
// Relative coordinates in [0, 1], optional
let x: Double?
let y: Double?
// Content
let text: String
let actionId: String?
}
struct OverlayPayload: Codable {
let imageURL: URL
let overlays: [OverlayDiv]
}
Optional JSON Schema fragment (server-side):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/overlay-div.schema.json",
"title": "OverlayDiv",
"type": "object",
"required": ["id", "kind", "alignment", "text"],
"properties": {
"id": { "type": "string" },
"kind": { "enum": ["label", "badge", "cta"] },
"alignment": {
"enum": [
"topLeading", "top", "topTrailing",
"leading", "center", "trailing",
"bottomLeading", "bottom", "bottomTrailing"
]
},
"x": { "type": "number", "minimum": 0, "maximum": 1 },
"y": { "type": "number", "minimum": 0, "maximum": 1 },
"text": { "type": "string", "maxLength": 120 },
"actionId": { "type": "string" }
}
}
Common failure at this step:
- Leaving
kindoralignmentas free-form strings. Always constrain them with enums to make validation effective and to keep the AI’s options safe and predictable.
3. Implement Server-Side Schema Validation
Now wire that schema into your backend so your AI agent’s UI proposal is validated before your app renders it.
Why this matters:
- The safest production pattern is schema-first, not code-first (JSON Schema is built for this).
- Thesys and others explicitly highlight validation and correction as critical in production AI apps.
- Apple’s Foundation Models framework emphasizes structured output and tool calling—typed contracts, not free-form blobs.
Example Node/Express validation with ajv:
import express from 'express';
import Ajv from 'ajv';
import schema from './overlay-div.schema.json';
const app = express();
app.use(express.json());
const ajv = new Ajv({ allErrors: true });
const validateOverlayPayload = ajv.compile({
type: 'object',
required: ['imageURL', 'overlays'],
properties: {
imageURL: { type: 'string', format: 'uri' },
overlays: {
type: 'array',
items: schema
}
}
});
app.post('/api/overlay-layout', (req, res) => {
const valid = validateOverlayPayload(req.body);
if (!valid) {
return res.status(400).json({
error: 'Invalid overlay payload',
details: validateOverlayPayload.errors
});
}
// Additional business rules: enforce max overlays, etc.
const overlays = req.body.overlays;
if (overlays.length > 6) {
return res.status(400).json({
error: 'Too many overlays (max 6).'
});
}
// If valid, return as-is (or enriched) to the client
res.json(req.body);
});
app.listen(3000, () => {
console.log('Overlay layout API listening on :3000');
});
Common failure at this step:
- Not enforcing upper bounds (e.g.,
maxLength, max overlay count). That can lead to unreadable UI or performance issues when the AI over-produces overlays.
4. Connect Uzori’s SDK to Your iOS App
Uzori’s iOS SDK is designed as a single-screen integration that plugs into your existing architecture. Your app sends:
- User intent (e.g., “Show me roaming protection options for my trip”) and
- Your backend API descriptions (OpenAPI)
Uzori’s engine:
- Calls tools / APIs
- Composes a SwiftUI screen from your schema
- Streams that screen into your app after server validation
High-level SwiftUI integration scaffold:
import SwiftUI
import UzoriSDK
struct UzoriOverlayScreen: View {
@State private var uzoriSession: UzoriSession? = nil
@State private var overlayPayload: OverlayPayload? = nil
var body: some View {
Group {
if let payload = overlayPayload {
GenerativeOverlayCard(payload: payload)
} else {
ProgressView("Loading...")
}
}
.task {
await startUzoriSession()
}
}
private func startUzoriSession() async {
do {
let session = try await UzoriSession.start(
configuration: .init(
backendBaseURL: URL(string: "https://api.example.com")!,
openAPISpecURL: URL(string: "https://api.example.com/openapi.json")!
)
)
uzoriSession = session
// Example: send an intent for a hero layout
let payload: OverlayPayload = try await session.requestOverlayLayout(
intent: "hero_roaming_protection"
)
await MainActor.run {
overlayPayload = payload
}
} catch {
print("Failed to start Uzori session: \(error)")
}
}
}
The exact Uzori API shape may differ, but the pattern is consistent:
- Start a session
- Describe your backend
- Ask for a layout (here,
OverlayPayload) - Receive only validated payloads
Common failure at this step:
- Treating Uzori as a generic chat API. Uzori is your AI interface layer—you should think in terms of “return me a SwiftUI screen / layout schema,” not “return some text I’ll parse.”
5. Render AI-Generated Overlays with SwiftUI’s Overlay System
With a validated OverlayPayload in hand, you can render the div-on-image pattern natively.
Apple documents that multiple overlays are collected into an implicit ZStack. We’ll map each OverlayDiv.Alignment to SwiftUI’s Alignment and render the right view depending on kind.
Alignment mapping helper:
extension OverlayDiv.Alignment {
var swiftUIAlignment: Alignment {
switch self {
case .topLeading: return .topLeading
case .top: return .top
case .topTrailing: return .topTrailing
case .leading: return .leading
case .center: return .center
case .trailing: return .trailing
case .bottomLeading: return .bottomLeading
case .bottom: return .bottom
case .bottomTrailing: return .bottomTrailing
}
}
}
Rendering the generative overlay card:
struct GenerativeOverlayCard: View {
let payload: OverlayPayload
var body: some View {
AsyncImage(url: payload.imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
baseImage(image)
case .failure:
Color.gray
@unknown default:
Color.gray
}
}
.frame(height: 240)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
@ViewBuilder
private func baseImage(_ image: Image) -> some View {
var view = image
.resizable()
.scaledToFill()
// Apply each overlay via SwiftUI's overlay system
for overlay in payload.overlays {
view = view.overlay(alignment: overlay.alignment.swiftUIAlignment) {
overlayView(for: overlay)
.padding(8)
}
}
view.clipped()
}
@ViewBuilder
private func overlayView(for overlay: OverlayDiv) -> some View {
switch overlay.kind {
case .label:
labelView(text: overlay.text)
case .badge:
badgeView(text: overlay.text)
case .cta:
ctaView(text: overlay.text, actionId: overlay.actionId)
}
}
private func labelView(text: String) -> some View {
Text(text)
.font(.headline)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
private func badgeView(text: String) -> some View {
Text(text.uppercased())
.font(.caption.bold())
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
private func ctaView(text: String, actionId: String?) -> some View {
Button {
if let actionId {
handleCTA(actionId: actionId)
}
} label: {
Text(text)
.font(.subheadline.bold())
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.thinMaterial)
.clipShape(Capsule())
}
}
private func handleCTA(actionId: String) {
// Route to a local action or delegate back to Uzori/your coordinator
print("CTA tapped: \(actionId)")
}
}
This keeps everything fully native:
- SwiftUI views
- Your design system and materials
- No arbitrary remote code execution
Common failure at this step:
- Trying to manually calculate absolute coordinates instead of leaning on
alignment. Use alignment for 90% of placements, and reserve x/y offsets for fine-tuning.
6. Add Relative Positioning (Optional Fine-Tuning)
Sometimes alignment isn’t enough. You might want the AI to place a badge at a precise relative position (e.g., 20% from the left, 70% from the top) to avoid covering a face in a photo.
We can use x and y in [0, 1] as relative coordinates and map them into the image’s geometry using a GeometryReader.
Relative overlay container:
struct RelativeOverlayCard: View {
let payload: OverlayPayload
var body: some View {
GeometryReader { proxy in
AsyncImage(url: payload.imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.height)
.overlay {
ZStack {
ForEach(payload.overlays) { overlay in
positionedOverlay(overlay, in: proxy.size)
}
}
}
case .failure:
Color.gray
@unknown default:
Color.gray
}
}
}
.frame(height: 240)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
@ViewBuilder
private func positionedOverlay(_ overlay: OverlayDiv, in size: CGSize) -> some View {
let x = overlay.x ?? 0.5
let y = overlay.y ?? 0.5
overlayView(for: overlay)
.position(
x: CGFloat(x) * size.width,
y: CGFloat(y) * size.height
)
}
@ViewBuilder
private func overlayView(for overlay: OverlayDiv) -> some View {
// Reuse same label/badge/cta views as before
switch overlay.kind {
case .label:
Text(overlay.text)
.font(.headline)
.padding(8)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
case .badge:
Text(overlay.text.uppercased())
.font(.caption.bold())
.padding(6)
.background(.ultraThinMaterial)
.clipShape(Capsule())
case .cta:
Button(overlay.text) {
if let actionId = overlay.actionId {
handleCTA(actionId: actionId)
}
}
.padding(6)
.background(.thinMaterial)
.clipShape(Capsule())
}
}
private func handleCTA(actionId: String) {
print("CTA tapped: \(actionId)")
}
}
Common failure at this step:
- Forgetting to clamp
xandyto[0, 1]on the server. If the AI sends1.3, your overlay will be off-screen. Enforceminimum: 0,maximum: 1in your schema.
7. Enforce Privacy and Safety in Your AI Layout Pipeline
Apple’s platform direction is clear:
- Foundation Models: emphasize structured output and tool calling.
- Human Interface Guidelines: request only the data you need.
- Private Cloud Compute: user data should not be accessible to anyone other than the user.
Your Uzori + SwiftUI div-on-image pipeline should follow the same principles.
Checklist for a safe, production-ready setup:
- Minimal context:
- Send only the fields the AI needs to decide overlay layout.
- Example: product name, price, image URL, key state flags.
- Strict schemas:
- Use JSON Schema for layouts; OpenAPI for APIs.
- Reject any payload with unexpected properties or values.
- Server-driven validation:
- Never render payloads validated only on-device.
- Always pass through your backend where you can log, inspect, and correct.
- Native rendering only:
- Uzori composes SwiftUI screens from a constrained, server-approved schema.
- No arbitrary code execution from AI output.
Common failure at this step:
- Letting the model “invent” fields or actions not described in your schema. Treat the schema as the contract; discard anything outside it.
FAQ: Troubleshooting Uzori Div-on-Image Overlays in SwiftUI
Why are my overlays all stacking in one corner of the image?
Most often, the alignment values coming from your payload don’t match your Swift enum mapping. For example, the AI might be emitting "top_left" while your enum expects "topLeading". Fix it by:
- Constraining allowed
alignmentvalues in schema (enum) - Adding a translation layer server-side (e.g., map
top_left→topLeading)
If you use relative positioning (x, y), ensure you’re not also forcing a conflicting alignment in the same view.
How do I stop the AI from generating too many overlays?
Enforce caps in two places:
- Schema-level hints: document recommended max overlays (e.g., 3–5) in your tool description to the AI.
- Server validation: reject any payload where
overlays.length > N(e.g., 6) and return an error.
Production frameworks like Thesys explicitly emphasize validation and correction—follow that pattern.
Can I reuse my existing design system components with Uzori?
Yes. Uzori’s generated layouts are described as schemas, not as hard-coded visuals. You can:
- Map
kind+stylefields to your existing SwiftUI components - Wrap overlays in your design system primitives (e.g.,
AppBadge,PrimaryButton)
Uzori’s goal is to be your AI interface layer, not to replace your design system.
What if the AI-generated text is too long and breaks the layout?
Protect your UI by:
- Setting
maxLengthconstraints fortextin your JSON Schema (e.g., 120 characters) - Truncating with
lineLimitandtruncationModein SwiftUI:
Text(overlay.text)
.lineLimit(2)
.truncationMode(.tail)
- Optionally, post-processing text server-side (e.g., summarizing to a shorter variant) before returning the payload.
How does Uzori compare to DivKit for this use case?
DivKit is a strong traditional server-driven UI reference: JSON-driven layouts, cross-platform, SwiftUI support. Uzori sits at the next layer:
- DivKit: server-driven, but non-generative; you hand-author JSON.
- Uzori: generative UI + server-driven safety; AI composes SwiftUI screens from your schema, and your server validates them.
If you want AI-native experiences (AI choosing which overlays to show, where, and when) while preserving native SwiftUI and safety, Uzori is focused specifically on that intersection.
By combining SwiftUI’s native overlay system, a strict schema for div-on-image layouts, server-side validation, and Uzori’s AI interface layer, you can let your app’s interface literally design itself—without sacrificing safety, performance, or native feel.