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:

Extreme close-up of magnetic tape ridges forming a diagonal pattern, symbolizing encoded video timeline segments.

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 VideoPlayer that 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’s video_sources.
  • chapters: each chapter is a segment with startTime and endTime.
  • 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 videoSources or 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 videoSource present
    • Only supported mimeType values (application/vnd.apple.mpegurl, video/mp4)
  • Chapter rules
    • chapters.count <= 50 (or a limit that fits your UX)
    • 0 <= startTime < endTime <= duration
    • Chapters monotonically non‑overlapping
  • Text limits
    • title.count <= 80
    • caption.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 DivVideoTimeline payloads 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:

  1. In Xcode: File → Add Packages…
  2. Enter Uzori’s Git URL (e.g., https://github.com/uzori/ios-sdk.git)
  3. 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 DivVideoTimeline schema
  • Let the AI agent call your media endpoint
  • Stream back a UI tree that includes a div-video-timeline node 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-timeline with 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 VideoPlayer gives you native playback controls on iOS 14+
  • ScrollView and HStack handle 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 AVPlayer from the first videoSource.
  • Tapping a chapter chip updates selectedChapterID, which triggers seekToSelectedChapter().
  • 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 preferredTimescale when 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 DivVideoTimeline contract
  • 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 getVideoTimeline when 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-timeline node.

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 DivVideoTimeline contract, 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 startTime and endTime are 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.

← All posts