SwiftUI Previews Reference
Table of Contents
- Preview Macro
- Preview with Mock Data
- @Previewable Property Wrappers
- Common Diagnostics
- 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
// 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:
#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:
// 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:
#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:
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:
@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:
#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.
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
// @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
#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.
#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:
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
#Previewfor new previews;PreviewProvideris still valid for older code - [ ] Provide a named preview for each meaningful state (default, empty, error, loading)
- [ ] Use
@Previewablefor interactive previews when targeting iOS 18+; wrapper views otherwise - [ ] Expose static
.sample/.previewdata 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
.defaultFocusover.onAppearwrites when seeding@FocusStatein previews