How to Build AI‑Generated Div Video Timelines in SwiftUI with Server Validation
By the end of this tutorial, you’ll have an AI‑generated, server‑validated video timeline running in a SwiftUI iOS app:

By the end of this tutorial, you’ll have an AI‑generated, server‑validated video timeline running in a SwiftUI iOS app:
- Chapters rendered as tappable chips or a rail
- Thumbnails and captions for each segment
- A SwiftUI
VideoPlayerthat can scrub and jump between chapters - A schema‑first “div video” contract that Uzori can safely generate and your server can strictly validate
All of it is powered by Uzori’s generative UI layer, so the AI composes the timeline layout while your backend enforces safety and structure.
Prerequisites
Before you start, you should have:
- Xcode 15+ and iOS 17+ target (Swift 5.9+)
- Familiarity with SwiftUI navigation and basic AVFoundation concepts
- An existing backend with at least one API endpoint returning video metadata
- An Uzori account and access to the Uzori iOS SDK (SwiftUI)
- Optional but useful: OpenAPI description for your media APIs
You do not need to be an AVFoundation expert. We’ll focus on the schema and SwiftUI wiring.
If you want a broader overview of div‑style media layouts (image, video, text overlays) first, see our related guide: Divs for Media Layout on iOS: A Pillar Guide to Image, Video and Text Overlays.
1. Define a schema‑first "div video timeline" contract
The foundation of AI‑generated video timelines in SwiftUI is a strict data contract. Uzori’s generative UI engine and your server must agree on a typed schema that:
- Describes the video source(s)
- Enumerates chapters with time ranges
- Encodes captions, titles, thumbnails
- Specifies safe limits (max chapters, max text length, etc.)
DivKit takes a similar approach for video: its div-video element requires either video_sources or player_settings_payload, and validates this on the server, raising an error if neither is provided (DivKit docs). We’ll mirror this philosophy.
1.1. A minimal Swift data model
Start by sketching a Swift struct that matches your JSON contract. This will be used both by Uzori’s renderer and your own unit tests:
struct DivVideoTimeline: Decodable {
struct VideoSource: Decodable {
let url: URL
let mimeType: String
}
struct Chapter: Decodable, Identifiable {
let id: String
let title: String
let startTime: TimeInterval
let endTime: TimeInterval
let thumbnailURL: URL?
let caption: String?
}
let id: String
let videoSources: [VideoSource]
let duration: TimeInterval
let chapters: [Chapter]
}
Key properties:
videoSources: array of playable URLs (e.g., HLS, MP4), similar to DivKit’svideo_sources.chapters: each chapter is a segment withstartTimeandendTime.caption: optional per‑chapter caption text.
1.2. JSON example your server will emit
This is the JSON Uzori’s AI will target and your backend will validate:
{
"id": "timeline_roaming_basics",
"videoSources": [
{
"url": "https://cdn.example.com/videos/roaming-plan.m3u8",
"mimeType": "application/vnd.apple.mpegurl"
}
],
"duration": 842.0,
"chapters": [
{
"id": "intro",
"title": "Intro to roaming",
"startTime": 0.0,
"endTime": 65.0,
"thumbnailURL": "https://cdn.example.com/thumbs/roaming-001.jpg",
"caption": "Why roaming fees exist and what your options are."
},
{
"id": "plans",
"title": "Plan types compared",
"startTime": 65.0,
"endTime": 310.0,
"thumbnailURL": "https://cdn.example.com/thumbs/roaming-002.jpg",
"caption": "Daily passes vs pay‑as‑you‑go with practical examples."
}
]
}
Common failure at this step:
- Missing required fields like
videoSourcesor invalid time ranges. Catch these early on the server instead of letting them crash the client.
2. Enforce server‑side validation and limits
AI‑generated UI must live inside strong guardrails. Uzori’s approach is “generative UI, server‑driven safety”: the AI proposes a div tree, your backend validates it.
DivKit does this by rejecting malformed div-video definitions (e.g., no video_sources) and supports validation across platforms. Its GitHub repo shows ~2.7k stars and 5,400+ commits, indicating a mature SDUI practice, not a toy experiment.
2.1. Validation rules to implement on the server
When your LLM or orchestration layer proposes a DivVideoTimeline, run these checks server‑side:
- Video source rules
- At least one
videoSourcepresent - Only supported
mimeTypevalues (application/vnd.apple.mpegurl,video/mp4)
- At least one
- Chapter rules
chapters.count <= 50(or a limit that fits your UX)0 <= startTime < endTime <= duration- Chapters monotonically non‑overlapping
- Text limits
title.count <= 80caption.count <= 240
- Thumbnail rules
- Thumbnails must be HTTPS URLs
If validation fails, return a structured error and either:
- Ask the LLM to regenerate within constraints, or
- Fallback to a simpler, non‑chapter timeline layout.
2.2. Exposing the contract via OpenAPI
Document this contract in OpenAPI so Uzori can discover it:
components:
schemas:
DivVideoTimeline:
type: object
required: [id, videoSources, duration, chapters]
properties:
id:
type: string
videoSources:
type: array
minItems: 1
items:
type: object
required: [url, mimeType]
properties:
url:
type: string
format: uri
mimeType:
type: string
duration:
type: number
format: float
chapters:
type: array
maxItems: 50
items:
type: object
required: [id, title, startTime, endTime]
properties:
id:
type: string
title:
type: string
maxLength: 80
startTime:
type: number
endTime:
type: number
thumbnailURL:
type: string
format: uri
caption:
type: string
maxLength: 240
Common failure at this step:
- Letting the LLM emit arbitrary JSON without validation. This leads to overlapping chapters, impossible durations, or non‑playable URLs.
3. Integrate Uzori’s SwiftUI SDK as a single AI screen
Uzori’s SDK is designed as a “one screen to integrate” layer. You mount an UzoriView (or whatever the current entry point is named in your version) that:
- Streams AI‑generated SwiftUI layouts from your server
- Receives validated
DivVideoTimelinepayloads as part of that layout - Renders them using native SwiftUI components
Exact names may vary slightly by SDK version; adapt these examples to the API you have.
3.1. Add Uzori SDK to your project
Typically via Swift Package Manager:
- In Xcode: File → Add Packages…
- Enter Uzori’s Git URL (e.g.,
https://github.com/uzori/ios-sdk.git) - Add the package to your app target
3.2. Mount a single AI‑powered screen
Create a SwiftUI view that hosts the Uzori interface:
import SwiftUI
import Uzori
struct AIVideoAssistantScreen: View {
var body: some View {
UzoriView(
configuration: .init(
endpoint: URL(string: "https://api.example.com/uzori/ai")!,
apiKey: "YOUR_API_KEY"
),
initialContext: [
"feature": "video_timeline",
"userId": "1234"
]
)
}
}
From here, Uzori will:
- Use your OpenAPI spec to discover the
DivVideoTimelineschema - Let the AI agent call your media endpoint
- Stream back a UI tree that includes a
div-video-timelinenode you’ll render next
Common failure at this step:
- Not scoping the AI feature. Always pass context like
"feature": "video_timeline"so the agent knows which tools and schemas to use.
4. Render the timeline as SwiftUI divs (chapters, thumbnails, captions)
Now you need a SwiftUI renderer for the DivVideoTimeline div. Think of this as the iOS equivalent of rendering DivKit div-video plus timeline controls.
4.1. A SwiftUI view for the timeline
import SwiftUI
struct DivVideoTimelineView: View {
let timeline: DivVideoTimeline
@Binding var selectedChapterID: String?
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(timeline.chapters) { chapter in
ChapterChipView(
chapter: chapter,
isSelected: chapter.id == selectedChapterID
)
.onTapGesture {
selectedChapterID = chapter.id
}
}
}
.padding(.horizontal)
}
}
}
struct ChapterChipView: View {
let chapter: DivVideoTimeline.Chapter
let isSelected: Bool
var body: some View {
VStack(alignment: .leading, spacing: 4) {
AsyncImage(url: chapter.thumbnailURL) { image in
image
.resizable()
.aspectRatio(16/9, contentMode: .fill)
.clipped()
} placeholder: {
Rectangle().opacity(0.1)
}
.frame(width: 160, height: 90)
.cornerRadius(8)
Text(chapter.title)
.font(.headline)
.lineLimit(2)
if let caption = chapter.caption {
Text(caption)
.font(.caption)
.lineLimit(2)
}
}
.padding(8)
.background(isSelected ? .thinMaterial : .regularMaterial)
.cornerRadius(12)
}
}
This view:
- Shows a horizontal rail of chapters
- Displays chapter thumbnails, titles, and short captions
- Binds selection via
selectedChapterID, which we’ll use to control playback
4.2. Connecting this to Uzori’s div renderer
In your Uzori integration, you’ll register a custom renderer for the div-video-timeline node type. In pseudocode:
Uzori.registerRenderer(for: "div-video-timeline") { node, context in
// Decode the timeline payload
let timeline: DivVideoTimeline = try context.decode(node.payload)
return AnyView(
AIVideoTimelineContainer(timeline: timeline)
)
}
AIVideoTimelineContainer will combine VideoPlayer with DivVideoTimelineView in the next step.
Common failure at this step:
- Making the renderer too generic. For performance and UX, render
div-video-timelinewith a dedicated view, not a single catch‑all JSON renderer.
5. Wire up SwiftUI VideoPlayer to scrub and jump between chapters
Apple’s media stack already supports rich timelines:
- AVFoundation can load chapter metadata and timed metadata
- SwiftUI’s
VideoPlayergives you native playback controls on iOS 14+ ScrollViewandHStackhandle the chapter rail
You just need to wire selection → playback position.
5.1. A container that coordinates playback and timeline
import SwiftUI
import AVKit
struct AIVideoTimelineContainer: View {
let timeline: DivVideoTimeline
@State private var player: AVPlayer?
@State private var selectedChapterID: String?
var body: some View {
VStack(spacing: 16) {
if let player {
VideoPlayer(player: player)
.onAppear {
player.play()
}
} else {
ProgressView("Loading video…")
}
DivVideoTimelineView(
timeline: timeline,
selectedChapterID: Binding(
get: { selectedChapterID },
set: { newValue in
selectedChapterID = newValue
seekToSelectedChapter()
}
)
)
}
.task {
await setupPlayer()
}
.padding()
}
private func setupPlayer() async {
guard let source = timeline.videoSources.first else {
return
}
let item = AVPlayerItem(url: source.url)
player = AVPlayer(playerItem: item)
}
private func seekToSelectedChapter() {
guard let id = selectedChapterID,
let chapter = timeline.chapters.first(where: { $0.id == id }),
let player
else { return }
let time = CMTime(seconds: chapter.startTime, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
player.play()
}
}
Behavior:
- When the view appears, it creates an
AVPlayerfrom the firstvideoSource. - Tapping a chapter chip updates
selectedChapterID, which triggersseekToSelectedChapter(). - The player seeks precisely to the chapter start time.
5.2. Optional: synchronize progress back to the timeline
For more advanced UX, you can:
- Observe
player.currentTime()periodically - Highlight the active chapter based on current playback time
- Expose playback position back to Uzori via a variable (similar to DivKit’s
elapsed_time_variable)
Common failure at this step:
- Ignoring
preferredTimescalewhen seeking. Always use a reasonable timescale (e.g., 600) for smooth, accurate scrubbing.
6. Add captions and accessibility‑friendly timelines
Apple emphasizes that captions are not just for Deaf or Hard‑of‑Hearing users; they’re also crucial in noisy environments. The Media Accessibility framework lets you adapt caption rendering to user preferences.
6.1. Respect system caption preferences
At minimum, ensure:
- Your video stream includes subtitles or closed captions.
- You enable them by default when system settings indicate a preference.
On the UI side, reflect chapter captions in your chips, as we already did with chapter.caption. Keep them short (<= 240 characters) so they’re scannable.
6.2. Make chapter navigation accessible
To ensure VoiceOver users can navigate chapters:
- Add accessibility labels on chapter chips:
ChapterChipView(
chapter: chapter,
isSelected: chapter.id == selectedChapterID
)
.accessibilityLabel(Text("Chapter: \(chapter.title)"))
.accessibilityHint(Text("Plays from \(Int(chapter.startTime)) seconds"))
- Keep the chapter order strictly chronological.
Common failure at this step:
- Treating captions as purely decorative. Use the same schema fields (
caption) both for visual timelines and for accessibility labels or hints.
7. Orchestrate everything through Uzori’s AI engine
Now you have:
- A schema‑first
DivVideoTimelinecontract - Strict server‑side validation and limits
- A SwiftUI renderer that can scrub, jump, and caption chapters
The last step is instructing Uzori’s AI to use it.
7.1. Tooling the AI with your video API
In Uzori’s configuration, expose your video metadata endpoint as a tool/function, e.g.:
getVideoTimeline(videoId: String) -> DivVideoTimeline
Then, in your AI prompt and policies:
- Encourage the agent to call
getVideoTimelinewhen the user asks to "watch", "jump to", or "compare" sections of a video. - Require that any UI responding to such queries must include a
div-video-timelinenode.
7.2. Example UX flows unlocked
With this setup, you can support:
- AI concierges for complex plans
- User: “Show me the part of this roaming video where you compare daily passes.”
- AI: Calls
getVideoTimeline, highlights the relevant chapter, seeks playback.
- Product walkthrough timelines
- AI generates a chaptered video explaining plan features with thumbnails and captions.
- Dynamic configuration flows
- AI presents alternate timelines for different plan options, each with its own set of chapters.
Common failure at this step:
- Not tying the AI’s authority to your schema. Make sure the agent can only emit video UI via the
DivVideoTimelinecontract, otherwise you lose safety guarantees.
FAQ: Troubleshooting AI‑Generated Div Video Timelines
1. Why is my SwiftUI VideoPlayer not starting at the selected chapter?
Most often, the issue is mismatched time units or tolerance:
- Ensure
startTimeandendTimeare in seconds on both server and client. - When seeking, use:
let time = CMTime(seconds: chapter.startTime, preferredTimescale: 600) player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
If you use a large tolerance, the player may snap to the nearest keyframe instead of your exact chapter start.
2. How do I prevent the AI from generating too many chapters?
Enforce a maxItems constraint in your OpenAPI schema (e.g., maxItems: 50 for chapters) and validate it server‑side. If the LLM proposes more items, reject the payload and ask it to regenerate within that limit.
3. Can I support multiple video qualities or sources?
Yes. Extend VideoSource with fields like quality or bitrate and let the AI include several sources. On the client, pick the best source based on network or device capabilities. Keep validation strict: each source must be a playable URL with a supported MIME type.
4. How does this compare to using DivKit directly on iOS?
DivKit is a strong open‑source SDUI foundation (about 2.7k GitHub stars, 5,448 commits) and already supports div-video across iOS, Android, and web. Uzori focuses on:
- Turning LLM responses into native SwiftUI screens
- Streaming UI as the user interacts
- Keeping generation safe via server‑validated schemas (similar spirit to DivKit, but AI‑native)
If you’re building an iOS‑first, SwiftUI‑native AI assistant, Uzori gives you a higher‑level generative UI layer on top of the SDUI patterns you might recognize from DivKit.
5. How do I handle captions and accessibility in multiple languages?
Add a languageCode field to your caption schema, and let the AI choose a language matching the user’s locale. Ensure your media streams include subtitles for those languages and that you respect system caption preferences via Media Accessibility APIs.
With a schema‑first div contract, strict server validation, and Uzori’s generative SwiftUI renderer, you can ship video timelines that feel like your app—not someone else’s chatbot. The AI handles the layout and chapter logic; you stay in control of safety, performance, and native UX.