← Files SwiftUI ExpertARCHIVED FILE

skills/swiftui-expert-skill/references/previews.md

8.13 KB · Oct 4, 2026 · 12:30 UTC

↓ Download file

# SwiftUI Previews Reference

## Table of Contents

- [Preview Macro](#preview-macro)
- [Preview with Mock Data](#preview-with-mock-data)
- [@Previewable Property Wrappers](#previewable-property-wrappers)
- [Common Diagnostics](#common-diagnostics)
- [Summary Checklist](#summary-checklist)

---

## Preview Macro

The `#Preview` macro (Swift 5.9+, Xcode 15+) is the modern way to declare previews. The legacy `PreviewProvider` protocol still works; prefer `#Preview` for new code because it's less verbose and supports inline traits.

### Basic Usage

```swift
// Modern: #Preview macro
#Preview {
    ContentView()
}

// Named preview
#Preview("Dark Mode") {
    ContentView()
        .preferredColorScheme(.dark)
}

// Legacy: PreviewProvider — still valid, but verbose for new code
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

### Multiple Previews

Declare one `#Preview` per meaningful state so each renders independently in the canvas:

```swift
#Preview("Default") {
    SettingsRow(title: "Notifications", isOn: true)
}

#Preview("Off State") {
    SettingsRow(title: "Notifications", isOn: false)
}

#Preview("Long Title") {
    SettingsRow(title: "Enable Push Notifications for All Events", isOn: true)
}
```

### Preview Traits

Traits configure the preview environment without modifying the view itself:

```swift
// Fixed size
#Preview(traits: .fixedLayout(width: 300, height: 100)) {
    CompactBanner(message: "Welcome")
}

// Size that fits content
#Preview(traits: .sizeThatFitsLayout) {
    BadgeView(count: 5)
}

// Landscape orientation
#Preview(traits: .landscapeLeft) {
    DashboardView()
}
```

### Previewing Inside NavigationStack

Wrap previewed destinations in their navigation container so toolbar items, titles, and back buttons render correctly:

```swift
#Preview {
    NavigationStack {
        DetailView(item: .sample)
    }
}
```

---

## Preview with Mock Data

Previews must compile and render without external dependencies. Live services, network calls, and disk I/O make previews slow, flaky, or broken; use self-contained sample data instead.

### Static Sample Data

Expose sample values as static properties on the model itself so any preview can reuse them without reconstructing values inline:

```swift
struct Item: Identifiable {
    let id: UUID
    var name: String
    var price: Double
}

extension Item {
    static let sample = Item(id: UUID(), name: "Widget", price: 9.99)

    static let samples: [Item] = [
        Item(id: UUID(), name: "Widget", price: 9.99),
        Item(id: UUID(), name: "Gadget", price: 19.99),
        Item(id: UUID(), name: "Doohickey", price: 4.99),
    ]
}

#Preview {
    ItemListView(items: Item.samples)
}
```

### Mock Observable Models

For views driven by an `@Observable` model (see `state-management.md` for fundamentals), expose pre-configured instances on the model itself:

```swift
@Observable
@MainActor
final class CartModel {
    var items: [Item] = []
    var isLoading = false

    static var preview: CartModel {
        let model = CartModel()
        model.items = Item.samples
        return model
    }

    static var emptyPreview: CartModel {
        CartModel()
    }

    static var loadingPreview: CartModel {
        let model = CartModel()
        model.isLoading = true
        return model
    }
}

#Preview("With Items") {
    CartView()
        .environment(CartModel.preview)
}

#Preview("Empty") {
    CartView()
        .environment(CartModel.emptyPreview)
}

#Preview("Loading") {
    CartView()
        .environment(CartModel.loadingPreview)
}
```

### Preview with Environment Dependencies

Inject any environment values the view depends on so the preview reflects a realistic runtime context:

```swift
#Preview {
    OrderDetailView(order: .sample)
        .environment(CartModel.preview)
        .environment(\.locale, Locale(identifier: "ja_JP"))
        .environment(\.dynamicTypeSize, .xxxLarge)
}
```

### Mocking Async Data Sources

When a view depends on a network or data service, give the dependency a protocol abstraction so previews can inject a synchronous mock that returns sample data immediately. This is one approach — adapt it to whatever pattern the surrounding codebase already uses.

```swift
protocol DataFetching {
    func fetchItems() async throws -> [Item]
}

struct LiveDataFetcher: DataFetching {
    let url: URL

    func fetchItems() async throws -> [Item] {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Item].self, from: data)
    }
}

struct MockDataFetcher: DataFetching {
    var result: Result<[Item], Error> = .success(Item.samples)

    func fetchItems() async throws -> [Item] {
        try result.get()
    }
}

#Preview {
    ItemListView(fetcher: MockDataFetcher())
}

#Preview("Error State") {
    ItemListView(fetcher: MockDataFetcher(result: .failure(URLError(.notConnectedToInternet))))
}
```

---

## @Previewable Property Wrappers

`@Previewable` (iOS 18+, Xcode 16+) lets you use `@State`, `@FocusState`, and other property wrappers directly inside a `#Preview` block, removing the need for a wrapper view to host interactive state.

### Interactive State

```swift
// @Previewable: interactive toggle inline in the preview
#Preview {
    @Previewable @State var isOn = false
    Toggle("Notifications", isOn: $isOn)
}

// Without @Previewable: requires a wrapper view
struct TogglePreviewWrapper: View {
    @State private var isOn = false
    var body: some View {
        Toggle("Notifications", isOn: $isOn)
    }
}

#Preview {
    TogglePreviewWrapper()
}
```

### Multiple Interactive Controls

```swift
#Preview {
    @Previewable @State var name = "Alice"
    @Previewable @State var age = 25.0

    VStack {
        TextField("Name", text: $name)
        Slider(value: $age, in: 0...100, step: 1) {
            Text("Age: \(Int(age))")
        }
        Text("Hello, \(name)! Age: \(Int(age))")
    }
    .padding()
}
```

### @Previewable with @FocusState

When seeding initial focus inside a preview, prefer `.defaultFocus` over writing to `@FocusState` from `.onAppear`. `.onAppear` can race the initial render and the focus assignment may be lost. See `focus-patterns.md` for the underlying rationale.

```swift
#Preview {
    @Previewable @FocusState var isFocused: Bool

    TextField("Search", text: .constant(""))
        .focused($isFocused)
        .defaultFocus($isFocused, true)
}
```

### Fallback for Pre-iOS 18 Targets

If the project's minimum deployment target is below iOS 18, `@Previewable` is unavailable. Fall back to a wrapper view:

```swift
private struct SliderPreview: View {
    @State private var value = 0.5
    var body: some View {
        CustomSlider(value: $value)
    }
}

#Preview {
    SliderPreview()
}
```

---

## Common Diagnostics

| Symptom | Cause | Fix |
|---|---|---|
| `#Preview` body type mismatch | The closure returns a non-`View` type | Make sure the final expression is a `View` |
| `@Previewable` only available in iOS 18+ | Using `@Previewable` with a lower deployment target | Use a wrapper view, or gate with `#available` |
| Preview crashes with "missing environment" | An `@Environment(SomeType.self)` value is not injected | Add `.environment(SomeType.preview)` to the preview |
| Preview hangs or renders blank | View depends on async data that never resolves | Inject a mock that returns immediately with sample data |
| `@MainActor`-isolated model accessed from non-isolated context | A preview helper touches main-actor-only API off the main actor | Mark the helper or the preview body `@MainActor` |

---

## Summary Checklist

- [ ] Prefer `#Preview` for new previews; `PreviewProvider` is still valid for older code
- [ ] Provide a named preview for each meaningful state (default, empty, error, loading)
- [ ] Use `@Previewable` for interactive previews when targeting iOS 18+; wrapper views otherwise
- [ ] Expose static `.sample` / `.preview` data on models so previews don't reconstruct values inline
- [ ] Inject mock services through a protocol when a view depends on async data
- [ ] Never depend on live network or disk I/O in a preview
- [ ] Prefer `.defaultFocus` over `.onAppear` writes when seeding `@FocusState` in previews

SHA-256: fdf50f169607ddb7e9c03218957df2b2d733e211f591ac86c99816ca058ede01