Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/previews.md
1# SwiftUI Previews Reference23## Table of Contents45- [Preview Macro](#preview-macro)6- [Preview with Mock Data](#preview-with-mock-data)7- [@Previewable Property Wrappers](#previewable-property-wrappers)8- [Common Diagnostics](#common-diagnostics)9- [Summary Checklist](#summary-checklist)1011---1213## Preview Macro1415The `#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.1617### Basic Usage1819```swift20// Modern: #Preview macro21#Preview {22ContentView()23}2425// Named preview26#Preview("Dark Mode") {27ContentView()28.preferredColorScheme(.dark)29}3031// Legacy: PreviewProvider — still valid, but verbose for new code32struct ContentView_Previews: PreviewProvider {33static var previews: some View {34ContentView()35}36}37```3839### Multiple Previews4041Declare one `#Preview` per meaningful state so each renders independently in the canvas:4243```swift44#Preview("Default") {45SettingsRow(title: "Notifications", isOn: true)46}4748#Preview("Off State") {49SettingsRow(title: "Notifications", isOn: false)50}5152#Preview("Long Title") {53SettingsRow(title: "Enable Push Notifications for All Events", isOn: true)54}55```5657### Preview Traits5859Traits configure the preview environment without modifying the view itself:6061```swift62// Fixed size63#Preview(traits: .fixedLayout(width: 300, height: 100)) {64CompactBanner(message: "Welcome")65}6667// Size that fits content68#Preview(traits: .sizeThatFitsLayout) {69BadgeView(count: 5)70}7172// Landscape orientation73#Preview(traits: .landscapeLeft) {74DashboardView()75}76```7778### Previewing Inside NavigationStack7980Wrap previewed destinations in their navigation container so toolbar items, titles, and back buttons render correctly:8182```swift83#Preview {84NavigationStack {85DetailView(item: .sample)86}87}88```8990---9192## Preview with Mock Data9394Previews 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.9596### Static Sample Data9798Expose sample values as static properties on the model itself so any preview can reuse them without reconstructing values inline:99100```swift101struct Item: Identifiable {102let id: UUID103var name: String104var price: Double105}106107extension Item {108static let sample = Item(id: UUID(), name: "Widget", price: 9.99)109110static let samples: [Item] = [111Item(id: UUID(), name: "Widget", price: 9.99),112Item(id: UUID(), name: "Gadget", price: 19.99),113Item(id: UUID(), name: "Doohickey", price: 4.99),114]115}116117#Preview {118ItemListView(items: Item.samples)119}120```121122### Mock Observable Models123124For views driven by an `@Observable` model (see `state-management.md` for fundamentals), expose pre-configured instances on the model itself:125126```swift127@Observable128@MainActor129final class CartModel {130var items: [Item] = []131var isLoading = false132133static var preview: CartModel {134let model = CartModel()135model.items = Item.samples136return model137}138139static var emptyPreview: CartModel {140CartModel()141}142143static var loadingPreview: CartModel {144let model = CartModel()145model.isLoading = true146return model147}148}149150#Preview("With Items") {151CartView()152.environment(CartModel.preview)153}154155#Preview("Empty") {156CartView()157.environment(CartModel.emptyPreview)158}159160#Preview("Loading") {161CartView()162.environment(CartModel.loadingPreview)163}164```165166### Preview with Environment Dependencies167168Inject any environment values the view depends on so the preview reflects a realistic runtime context:169170```swift171#Preview {172OrderDetailView(order: .sample)173.environment(CartModel.preview)174.environment(\.locale, Locale(identifier: "ja_JP"))175.environment(\.dynamicTypeSize, .xxxLarge)176}177```178179### Mocking Async Data Sources180181When 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.182183```swift184protocol DataFetching {185func fetchItems() async throws -> [Item]186}187188struct LiveDataFetcher: DataFetching {189let url: URL190191func fetchItems() async throws -> [Item] {192let (data, _) = try await URLSession.shared.data(from: url)193return try JSONDecoder().decode([Item].self, from: data)194}195}196197struct MockDataFetcher: DataFetching {198var result: Result<[Item], Error> = .success(Item.samples)199200func fetchItems() async throws -> [Item] {201try result.get()202}203}204205#Preview {206ItemListView(fetcher: MockDataFetcher())207}208209#Preview("Error State") {210ItemListView(fetcher: MockDataFetcher(result: .failure(URLError(.notConnectedToInternet))))211}212```213214---215216## @Previewable Property Wrappers217218`@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.219220### Interactive State221222```swift223// @Previewable: interactive toggle inline in the preview224#Preview {225@Previewable @State var isOn = false226Toggle("Notifications", isOn: $isOn)227}228229// Without @Previewable: requires a wrapper view230struct TogglePreviewWrapper: View {231@State private var isOn = false232var body: some View {233Toggle("Notifications", isOn: $isOn)234}235}236237#Preview {238TogglePreviewWrapper()239}240```241242### Multiple Interactive Controls243244```swift245#Preview {246@Previewable @State var name = "Alice"247@Previewable @State var age = 25.0248249VStack {250TextField("Name", text: $name)251Slider(value: $age, in: 0...100, step: 1) {252Text("Age: \(Int(age))")253}254Text("Hello, \(name)! Age: \(Int(age))")255}256.padding()257}258```259260### @Previewable with @FocusState261262When 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.263264```swift265#Preview {266@Previewable @FocusState var isFocused: Bool267268TextField("Search", text: .constant(""))269.focused($isFocused)270.defaultFocus($isFocused, true)271}272```273274### Fallback for Pre-iOS 18 Targets275276If the project's minimum deployment target is below iOS 18, `@Previewable` is unavailable. Fall back to a wrapper view:277278```swift279private struct SliderPreview: View {280@State private var value = 0.5281var body: some View {282CustomSlider(value: $value)283}284}285286#Preview {287SliderPreview()288}289```290291---292293## Common Diagnostics294295| Symptom | Cause | Fix |296|---|---|---|297| `#Preview` body type mismatch | The closure returns a non-`View` type | Make sure the final expression is a `View` |298| `@Previewable` only available in iOS 18+ | Using `@Previewable` with a lower deployment target | Use a wrapper view, or gate with `#available` |299| Preview crashes with "missing environment" | An `@Environment(SomeType.self)` value is not injected | Add `.environment(SomeType.preview)` to the preview |300| Preview hangs or renders blank | View depends on async data that never resolves | Inject a mock that returns immediately with sample data |301| `@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` |302303---304305## Summary Checklist306307- [ ] Prefer `#Preview` for new previews; `PreviewProvider` is still valid for older code308- [ ] Provide a named preview for each meaningful state (default, empty, error, loading)309- [ ] Use `@Previewable` for interactive previews when targeting iOS 18+; wrapper views otherwise310- [ ] Expose static `.sample` / `.preview` data on models so previews don't reconstruct values inline311- [ ] Inject mock services through a protocol when a view depends on async data312- [ ] Never depend on live network or disk I/O in a preview313- [ ] Prefer `.defaultFocus` over `.onAppear` writes when seeding `@FocusState` in previews314