Tutorial
How to set up SwiftUI previews in Xcode (#Preview macro guide)
Set up SwiftUI previews with the modern #Preview macro in Xcode 15+. Multiple variants, UIKit/AppKit previews, and the iOS 17+ deployment gotcha.
If you’ve opened a SwiftUI file in Xcode and seen PreviewProvider boilerplate that nobody fully understands, this is the modern replacement. The #Preview macro arrived in Xcode 15 and is now the standard for any project targeting iOS 17 / macOS 14 or newer.
Verified 2026-04-27 against Apple’s Previews in Xcode docs and SwiftLee’s macro deep-dive. The
#Previewmacro requires Swift 5.9+ and Xcode 15+. Xcode 26 also added a separate#Playgroundmacro for canvas-based code snippets — distinct from#Preview.
What the #Preview macro is
A Swift macro (introduced in Swift 5.9) that registers a view to render in Xcode’s preview canvas — without the protocol/struct/static-var boilerplate of the old PreviewProvider.
Old way (PreviewProvider, still works for legacy):
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Modern way (#Preview macro):
#Preview {
ContentView()
}
That’s the entire migration. The macro generates equivalent code at compile time. You can right-click the macro and choose Expand Macro to see exactly what it expands to.
The fastest setup
-
Open
ContentView.swift(or whichever view file) -
Below the
struct ContentView: View { ... }definition, add:#Preview { ContentView() } -
Show the preview canvas: Editor → Canvas or
Option+Cmd+Return -
If previews aren’t running, click Resume in the canvas (or
Option+Cmd+P)
Multiple previews — title each one
Stack #Preview declarations to compare states. Pass a title as the first parameter:
#Preview("Empty") {
ContentView(items: [])
}
#Preview("With data") {
ContentView(items: Item.samples)
}
#Preview("Dark mode") {
ContentView(items: Item.samples)
.preferredColorScheme(.dark)
}
#Preview("Spanish") {
ContentView(items: Item.samples)
.environment(\.locale, Locale(identifier: "es"))
}
Each becomes a separate card in the canvas. Use the canvas’s variant picker to switch between them quickly.
Previewing UIKit / AppKit views (the big upgrade over PreviewProvider)
PreviewProvider could only preview View-conforming SwiftUI types. The #Preview macro accepts any view-returning closure, including UIViewController and NSViewController:
#Preview {
let vc = ArticleViewController()
vc.titleLabel.text = "Previews for UIKit work!"
return vc
}
Same pattern for AppKit on macOS. For mixed-stack projects (legacy UIKit + new SwiftUI), this is the killer feature — you can preview the UIKit parts without wrapping them in a UIViewControllerRepresentable.
The iOS 17+ deployment gotcha
This catches everyone with a project that targets older iOS:
'Preview' is only available in iOS 17.0 or newer
#Preview requires iOS 17 / macOS 14 / watchOS 10 / tvOS 17 minimum, even if you’re only previewing on a newer simulator. Apple acknowledged this is a known limitation — there’s no compiler-directive workaround that ships in Xcode today.
Three options if your project supports iOS 16:
- Bump your deployment target. iOS 17+ adoption is now ~95% (Apple’s own stats). Most apps can drop iOS 16 with minimal user impact.
- Keep using
PreviewProvider— it still works on every Swift version. Use the modern macro for new files added with iOS-17-only views. - Mixed approach — if a specific view requires iOS 17 features anyway, mark it
@available(iOS 17.0, *)and use#Previewonly there. For shared views,PreviewProvider.
Common preview failures and fixes
1. “Cannot preview in this file” / immediate crash on Resume.
Most often: your view’s init requires a property you didn’t supply. Check the canvas Diagnostics pane (top-right corner of the preview area) for the actual error.
// ❌ Will crash — UserView needs a User
#Preview { UserView() }
// ✅ Pass a sample
#Preview { UserView(user: User.sample) }
2. View depends on @Environment values not provided.
Inject them in the closure:
#Preview {
OrderDetailView(orderID: "abc-123")
.environment(\.colorScheme, .dark)
.environment(\.locale, Locale(identifier: "en"))
}
3. View needs a SwiftData / Core Data context.
Spin up an in-memory container so previews don’t touch the real store:
#Preview {
let container = try! ModelContainer(
for: Item.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
container.mainContext.insert(Item.sample)
return ItemListView()
.modelContainer(container)
}
4. Network calls or main-thread blocking in init.
Move them to .task { ... } and feed mock data in the preview:
#Preview {
ProductView(product: Product.mock) // already-loaded mock, no fetch
}
5. Stale build cache giving cryptic errors.
Cmd+Option+P forces a preview rebuild. If that doesn’t fix it, Product → Clean Build Folder (Cmd+Shift+K), then resume.
Tips that earn back hours
- Default a sample type on your model. Add
static let sample = Item(name: "...", ...)to every model. Previews become one-liners. - One file = one #Preview block per state. Don’t try to test all states with conditional logic inside one preview — multiple
#Previewmacros render faster and are easier to scan. - Use the
traits:parameter for canvas configuration.#Preview(traits: .landscapeLeft) { ... }rotates a single preview without setting it on the simulator. Other traits include.sizeThatFitsLayout,.fixedLayout(width:height:). - Live previews are interactive — click the play button on the canvas to enable taps, gestures, animation. Way faster than running the simulator.
When to use #Playground (Xcode 26 only) instead
Xcode 26 added a separate #Playground macro for executing arbitrary code snippets in the canvas — not for previewing views, but for trying out functions and seeing their output inline. Different tool, same canvas. If you’re on Xcode 25 or earlier, ignore it.
Tired of looking up Xcode shortcuts every five minutes?
Skilly is a voice-first AI tutor for Mac that watches your Xcode window, hears your question, and points at exactly the menu, button, or panel you need — answer streaming as text right beside the cursor. Same flow for SwiftUI, AppKit, Blender, Figma, anything on Mac. 15 minutes free, no card.
FAQ