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
#Preview matrix — 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
@Model and 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
previewContainer seeded with sample models, attached via .modelContainer(previewContainer).
- Xcode 16 / iOS 18 → offer a
PreviewModifier so the seeded container/data is built once and cached across every preview, applied with #Preview(traits: .modifier(SampleData())).
- View needs
@State/@Bindable → use @Previewable so state lives directly in the #Preview body.
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.swift for large matrices. Ask if unsure.
- Wrap preview-only data in
#if DEBUG so 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
#Preview blocks 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-setup already 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
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.
1---
2name: preview-data-generator
3description: Generate sample data and a multi-variant #Preview matrix for SwiftUI views — empty/loading/error/loaded states, light/dark, Dynamic Type, locales/RTL, and devices. Use when the user says "add previews", "sample data for previews", "preview my view in different states", "preview data", "prototype this UI", or wants realistic Xcode canvas data without hand-rolling it.
4allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion]
5last_verified: 2026-07-16
6review_by: 2027-06-22
7os_version: iOS 27 / macOS 27
8---
9
10# Preview Data Generator
11
12Generates two tightly-coupled things for a SwiftUI view:
13
141. **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).
152. **A `#Preview` matrix** — 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.
16
17This 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.
18
19## When This Skill Activates
20
21Use this skill when the user:
22- Wants sample/mock data for Xcode previews ("what do I put in the preview?")
23- Wants to preview a view in multiple states (empty / loading / error / loaded)
24- Is prototyping UI and wants light/dark, Dynamic Type, RTL, or device variants
25- Says "add previews", "preview matrix", "preview this in dark mode + large text"
26- Has a SwiftData `@Model` and needs an in-memory seeded container for previews
27- Is on Xcode 16 / iOS 18 and wants shared, cached preview data via `PreviewModifier`
28
29**Just need fixtures for unit tests?** Use `testing/test-data-factory` instead.
30**Want screenshot regression tests?** Pair this with `testing/snapshot-test-setup` — the same data + variant matrix feeds snapshot tests.
31
32## Reference Files
33
34Load both before generating:
35
36| File | Purpose |
37|------|---------|
38| **preview-data-patterns.md** | Sample-data design, the edge-case catalog, SwiftData in-memory seeding, `PreviewModifier` (iOS 18), `@Previewable`, reusing `test-data-factory` |
39| **preview-matrix.md** | The variant axes, preview `traits:`, `.environment` overrides, deployment-target fallbacks (`#Preview` vs `PreviewProvider`), data-state previews |
40
41## Pre-Generation Checks
42
43Generators are context-aware. Before writing code, detect:
44
45| Check | How | Why it matters |
46|-------|-----|----------------|
47| **Deployment target** | Read project/`.xcodeproj` or `Package.swift` | iOS 17+ → `#Preview` macro; iOS 18+ → `PreviewModifier` + `@Previewable`; below 17 → `PreviewProvider` fallback |
48| **Target view + its models** | Read the view file; Grep its `init`/properties for model types | Determines which types need sample data |
49| **Existing fixtures** | `Grep "static func fixture\|extension .*{ static (let\|var) preview"` | Reuse `Model.fixture()` / existing `.preview` — never duplicate |
50| **SwiftData** | `Grep "@Model"` on the model types | Use the in-memory `.modelContainer(inMemory:)` seed pattern, not plain structs |
51| **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 |
52| **Platform** | iOS / macOS / multiplatform | Device variants and some traits differ |
53
54Ask via AskUserQuestion only what you can't infer — e.g. "Which states matter for this view: empty, loading, error, loaded, or all four?"
55
56## Generation Process
57
58### Step 1: Build the Sample Data
59
60For 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):
61
62```swift
63extension Article {
64 /// A typical, realistic instance for the canvas.
65 static var preview: Article {
66 Article(id: UUID(), title: "Designing for the Smallest Screen",
67 author: "Mei Chen", body: String(repeating: "Lorem ipsum. ", count: 40),
68 imageURL: URL(string: "https://picsum.photos/seed/1/600/400"),
69 readMinutes: 6, isBookmarked: false)
70 }
71
72 /// Edge cases that expose layout bugs.
73 static var previewLongTitle: Article { .preview.with(title: "An Extraordinarily, Almost Unreasonably Long Headline That Wraps") }
74 static var previewNoImage: Article { .preview.with(imageURL: nil) }
75 static var previewList: [Article] { (1...12).map { .preview.with(id: UUID(), title: "Article \($0)") } }
76 static var previewEmpty: [Article] { [] }
77}
78```
79
80If `Article.fixture()` already exists (from `test-data-factory`), build `.preview` *on top of it* rather than re-specifying every field.
81
82### Step 2: Build the Preview Matrix
83
84Generate the `#Preview` blocks for the axes that matter (see **preview-matrix.md**). Always include **data states** — that's the UI-prototyping payoff:
85
86```swift
87#Preview("Loaded") { ArticleListView(state: .loaded(Article.previewList)) }
88#Preview("Empty") { ArticleListView(state: .empty) }
89#Preview("Loading") { ArticleListView(state: .loading) }
90#Preview("Error") { ArticleListView(state: .error("No connection")) }
91
92#Preview("Dark") { ArticleListView(state: .loaded(Article.previewList)).preferredColorScheme(.dark) }
93#Preview("XXL Text") { ArticleListView(state: .loaded(Article.previewList)).dynamicTypeSize(.accessibility3) }
94#Preview("German / RTL", traits: .sizeThatFitsLayout) {
95 ArticleDetailView(article: .previewLongTitle).environment(\.locale, .init(identifier: "de"))
96}
97```
98
99Scale 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.
100
101### Step 3: Wire Up Infrastructure (when needed)
102
103- **SwiftData view** → generate an in-memory `previewContainer` seeded with sample models, attached via `.modelContainer(previewContainer)`.
104- **Xcode 16 / iOS 18** → offer a `PreviewModifier` so the seeded container/data is built **once and cached** across every preview, applied with `#Preview(traits: .modifier(SampleData()))`.
105- **View needs `@State`/`@Bindable`** → use `@Previewable` so state lives directly in the `#Preview` body.
106
107### Step 4: Place the Code
108
109- Sample data → `Article+Preview.swift` (one file per model, in the model's group).
110- Preview blocks → either appended to the view file (Apple's convention) or a sibling `ArticleListView+Previews.swift` for large matrices. Ask if unsure.
111- Wrap preview-only data in `#if DEBUG` so it never ships in release builds.
112
113## Output Format
114
115After generating, always provide:
116
117- **Files created** — full paths (`Article+Preview.swift`, `PreviewSupport.swift`, etc.)
118- **Integration steps** — where the `#Preview` blocks went, how to open the canvas
119- **Deployment-target notes** — which API was used and why (`#Preview` / `PreviewModifier` / `PreviewProvider`)
120- **Testing instructions** — open the canvas, cycle the variants; if pairing with snapshots, how the data feeds `snapshot-test-setup`
121
122## Example Output Summary
123
124```
125✅ Generated previews for ArticleListView (iOS 18 target)
126
127Files created:
128 • Article+Preview.swift — .preview, .previewLongTitle, .previewNoImage, .previewList, .previewEmpty
129 • PreviewSupport.swift — SampleData: PreviewModifier (cached in-memory container)
130 • ArticleListView+Previews.swift — 7 #Preview blocks (4 states, dark, XXL text, German)
131
132Integration:
133 • Reused Article.fixture() from test-data-factory for base fields
134 • Open ArticleListView+Previews.swift → canvas shows all 7 variants
135
136API used: #Preview + PreviewModifier (.modifier) — shared container built once, cached.
137```
138
139## When NOT to Use This Skill
140
141- **Fixtures for the test suite** → `testing/test-data-factory`
142- **Snapshot/regression image tests** → `testing/snapshot-test-setup` (feed it this data)
143- **A trivial view with no data** → Xcode's autogenerated `#Preview {}` is enough
144- **Locale-only preview helpers** → `generators/localization-setup` already covers locale preview helpers; reuse them
145
146## Cross-Skill Integration
147
148```
149generators/persistence-setup → defines @Model types
150 ↓
151testing/test-data-factory → Model.fixture() for tests
152 ↓
153generators/preview-data-generator (THIS SKILL)
154 • reuses .fixture() for canvas data
155 • builds the #Preview state/appearance matrix
156 ↓
157testing/snapshot-test-setup → renders the same matrix for regression
158```
159
160## Deliverables
161
162- [ ] Sample data per model: realistic instance + edge cases (empty, long, missing, list)
163- [ ] Existing `.fixture()`/`.preview` reused, not duplicated
164- [ ] `#Preview` matrix covering the view's data states + relevant appearance axes
165- [ ] Correct API for the deployment target (`#Preview` / `PreviewModifier` / `PreviewProvider`)
166- [ ] SwiftData views get a seeded in-memory container
167- [ ] Preview-only code guarded with `#if DEBUG`
168- [ ] Output summary with files, integration, and testing steps
169
170---
171
172**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.