Preview Data Generator
Generates two tightly-coupled things for a SwiftUI view:
- Sample data tuned for the Xcode canvas — realistic instances plus the visual edge cases that break layouts (empty, one item, huge list, long/overflowing strings, missing images, error and loading states).
- A
#Previewmatrix — the variant blocks you'd otherwise hand-write to prototype and QA a view across light/dark, Dynamic Type, locale/RTL, device sizes, and data states.
This is the design-time counterpart to testing/test-data-factory (which makes fixtures for the test suite). Where a factory already exists, this skill reuses Model.fixture() instead of inventing parallel data.
When This Skill Activates
Use this skill when the user:
- Wants sample/mock data for Xcode previews ("what do I put in the preview?")
- Wants to preview a view in multiple states (empty / loading / error / loaded)
- Is prototyping UI and wants light/dark, Dynamic Type, RTL, or device variants
- Says "add previews", "preview matrix", "preview this in dark mode + large text"
- Has a SwiftData
@Modeland needs an in-memory seeded container for previews - Is on Xcode 16 / iOS 18 and wants shared, cached preview data via
PreviewModifier
Just need fixtures for unit tests? Use testing/test-data-factory instead.
Want screenshot regression tests? Pair this with testing/snapshot-test-setup — the same data + variant matrix feeds snapshot tests.
Reference Files
Load both before generating:
| File | Purpose |
|---|---|
| preview-data-patterns.md | Sample-data design, the edge-case catalog, SwiftData in-memory seeding, PreviewModifier (iOS 18), @Previewable, reusing test-data-factory |
| preview-matrix.md | The variant axes, preview traits:, .environment overrides, deployment-target fallbacks (#Preview vs PreviewProvider), data-state previews |
Pre-Generation Checks
Generators are context-aware. Before writing code, detect:
| Check | How | Why it matters |
|---|---|---|
| Deployment target | Read project/.xcodeproj or Package.swift |
iOS 17+ → #Preview macro; iOS 18+ → PreviewModifier + @Previewable; below 17 → PreviewProvider fallback |
| Target view + its models | Read the view file; Grep its init/properties for model types |
Determines which types need sample data |
| Existing fixtures | Grep "static func fixture|extension .*{ static (let|var) preview" |
Reuse Model.fixture() / existing .preview — never duplicate |
| SwiftData | Grep "@Model" on the model types |
Use the in-memory .modelContainer(inMemory:) seed pattern, not plain structs |
| View shape | Does it take a model, a ViewModel, or fetch its own data? | Drives whether to inject data, a mock VM, or a seeded container |
| Platform | iOS / macOS / multiplatform | Device variants and some traits differ |
Ask via AskUserQuestion only what you can't infer — e.g. "Which states matter for this view: empty, loading, error, loaded, or all four?"
Generation Process
Step 1: Build the Sample Data
For each model the view needs, generate a Model.preview namespace with the realistic case and the edge cases (see preview-data-patterns.md for the full catalog):
extension Article {
/// A typical, realistic instance for the canvas.
static var preview: Article {
Article(id: UUID(), title: "Designing for the Smallest Screen",
author: "Mei Chen", body: String(repeating: "Lorem ipsum. ", count: 40),
imageURL: URL(string: "https://picsum.photos/seed/1/600/400"),
readMinutes: 6, isBookmarked: false)
}
/// Edge cases that expose layout bugs.
static var previewLongTitle: Article { .preview.with(title: "An Extraordinarily, Almost Unreasonably Long Headline That Wraps") }
static var previewNoImage: Article { .preview.with(imageURL: nil) }
static var previewList: [Article] { (1...12).map { .preview.with(id: UUID(), title: "Article \($0)") } }
static var previewEmpty: [Article] { [] }
}
If Article.fixture() already exists (from test-data-factory), build .preview on top of it rather than re-specifying every field.
Step 2: Build the Preview Matrix
Generate the #Preview blocks for the axes that matter (see preview-matrix.md). Always include data states — that's the UI-prototyping payoff:
#Preview("Loaded") { ArticleListView(state: .loaded(Article.previewList)) }
#Preview("Empty") { ArticleListView(state: .empty) }
#Preview("Loading") { ArticleListView(state: .loading) }
#Preview("Error") { ArticleListView(state: .error("No connection")) }
#Preview("Dark") { ArticleListView(state: .loaded(Article.previewList)).preferredColorScheme(.dark) }
#Preview("XXL Text") { ArticleListView(state: .loaded(Article.previewList)).dynamicTypeSize(.accessibility3) }
#Preview("German / RTL", traits: .sizeThatFitsLayout) {
ArticleDetailView(article: .previewLongTitle).environment(\.locale, .init(identifier: "de"))
}
Scale the matrix to the request — don't emit 20 previews for a label. A reasonable default per view: the relevant data states + dark mode + one large Dynamic Type + (if the view has text that localizes) one long-language/RTL check.
Step 3: Wire Up Infrastructure (when needed)
- SwiftData view → generate an in-memory
previewContainerseeded with sample models, attached via.modelContainer(previewContainer). - Xcode 16 / iOS 18 → offer a
PreviewModifierso the seeded container/data is built once and cached across every preview, applied with#Preview(traits: .modifier(SampleData())). - View needs
@State/@Bindable→ use@Previewableso state lives directly in the#Previewbody.
Step 4: Place the Code
- Sample data →
Article+Preview.swift(one file per model, in the model's group). - Preview blocks → either appended to the view file (Apple's convention) or a sibling
ArticleListView+Previews.swiftfor large matrices. Ask if unsure. - Wrap preview-only data in
#if DEBUGso it never ships in release builds.
Output Format
After generating, always provide:
- Files created — full paths (
Article+Preview.swift,PreviewSupport.swift, etc.) - Integration steps — where the
#Previewblocks went, how to open the canvas - Deployment-target notes — which API was used and why (
#Preview/PreviewModifier/PreviewProvider) - Testing instructions — open the canvas, cycle the variants; if pairing with snapshots, how the data feeds
snapshot-test-setup
Example Output Summary
✅ Generated previews for ArticleListView (iOS 18 target)
Files created:
• Article+Preview.swift — .preview, .previewLongTitle, .previewNoImage, .previewList, .previewEmpty
• PreviewSupport.swift — SampleData: PreviewModifier (cached in-memory container)
• ArticleListView+Previews.swift — 7 #Preview blocks (4 states, dark, XXL text, German)
Integration:
• Reused Article.fixture() from test-data-factory for base fields
• Open ArticleListView+Previews.swift → canvas shows all 7 variants
API used: #Preview + PreviewModifier (.modifier) — shared container built once, cached.
When NOT to Use This Skill
- Fixtures for the test suite →
testing/test-data-factory - Snapshot/regression image tests →
testing/snapshot-test-setup(feed it this data) - A trivial view with no data → Xcode's autogenerated
#Preview {}is enough - Locale-only preview helpers →
generators/localization-setupalready covers locale preview helpers; reuse them
Cross-Skill Integration
generators/persistence-setup → defines @Model types
↓
testing/test-data-factory → Model.fixture() for tests
↓
generators/preview-data-generator (THIS SKILL)
• reuses .fixture() for canvas data
• builds the #Preview state/appearance matrix
↓
testing/snapshot-test-setup → renders the same matrix for regression
Deliverables
- Sample data per model: realistic instance + edge cases (empty, long, missing, list)
- Existing
.fixture()/.previewreused, not duplicated -
#Previewmatrix covering the view's data states + relevant appearance axes - Correct API for the deployment target (
#Preview/PreviewModifier/PreviewProvider) - SwiftData views get a seeded in-memory container
- Preview-only code guarded with
#if DEBUG - Output summary with files, integration, and testing steps
Generate the data and the variants together. A preview is only as useful as the data in it — and a view is only proven when you've seen it empty, overflowing, in the dark, and at AX5.