civil-and-structural-engineering
How to Use Swiftui Previews for Faster Ui Development
Table of Contents
SwiftUI has fundamentally changed how iOS developers build user interfaces, but the true accelerator is the preview system built into Xcode. Instead of repeatedly compiling and running your app on a simulator or device to see a layout change, SwiftUI Previews let you see modifications instantly in a side canvas. For experienced developers, this shift from build-and-run to live-update dramatically shortens the feedback loop — and that translates directly into faster, more confident UI development.
What Are SwiftUI Previews?
SwiftUI Previews are interactive renderings of your view code that appear directly inside Xcode’s editor area. When you make a change to a SwiftUI view, the preview automatically refreshes within seconds, showing the new layout, colors, text, or dynamic content. Under the hood, Xcode compiles just the view you’re editing (not the entire app) and runs it in a lightweight sandbox. This is why previews are so much faster than launching a simulator.
Previews are not merely static mockups. They support user interactions — tapping a button, scrolling a list, entering text — and can even display data from live sources like Core Data or network responses when configured correctly. For many projects, previews become the primary way to iterate on design, relegating full simulator or device testing to later stages.
Setting Up Previews: From PreviewProvider to the #Preview Macro
Historically, SwiftUI previews required a struct conforming to the PreviewProvider protocol. That syntax still works in all current versions of Xcode, but Apple introduced the #Preview macro starting in iOS 17 / Xcode 15. Both approaches are valid, but the macro is more concise and easier to read.
Using PreviewProvider (legacy but universal)
To create a preview with PreviewProvider, add a nested struct inside your view file:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
The struct name typically follows the convention YourView_Previews. Xcode automatically discovers these providers when you open the canvas (Option+Command+Return) or click the Resume button.
Using the #Preview Macro (modern, iOS 17+)
The new macro simplifies preview code to a single line or block. Inside your view file (not inside the view struct itself), write:
#Preview {
MyView()
}
You can also name your previews with a string literal, which helps when you have multiple previews:
#Preview("Default State") {
MyView()
}
#Preview("Loading State") {
MyView(state: .loading)
}
The macro automatically infers the previewed view and removes boilerplate. Because it’s just a macro, you can still apply view modifiers like .previewDevice() and .preferredColorScheme() inside the block.
Customizing Previews for Real-World Testing
A single preview of your view on one device in light mode is rarely enough. Modern apps must look good on iPhones, iPads, and sometimes Macs, in both dark and light modes, with different font sizes and localizations. SwiftUI previews make it trivial to test all these variations side by side.
Device Selection
Use the .previewDevice() modifier to specify any simulator device name as a string. For example:
MyView()
.previewDevice("iPhone 15 Pro")
You can also pass nil to let Xcode pick the default device. For testing multiple devices, wrap them in a Group or use the #Preview macro multiple times:
Group {
MyView().previewDevice("iPhone SE (3rd generation)")
MyView().previewDevice("iPad Pro 12.9-inch (6th generation)")
}
Color Scheme: Dark and Light Mode
Dark mode is a common source of UI bugs. Force a specific scheme with .preferredColorScheme(). You can also combine this with device variations:
Group {
MyView().preferredColorScheme(.light)
MyView().preferredColorScheme(.dark)
}
Dynamic Type and Accessibility
To verify that your layout handles larger or smaller text, apply the .dynamicTypeSize() modifier. Testing extremes like .dynamicTypeSize(.accessibility5) helps you catch truncation or overlapping text before users do:
MyView()
.dynamicTypeSize(.large)
.previewDisplayName("Normal Size")
MyView()
.dynamicTypeSize(.accessibility5)
.previewDisplayName("Large Accessibility")
Orientation and Layout
You can simulate landscape or portrait using the .previewInterfaceOrientation() modifier (iOS 16+):
MyView()
.previewInterfaceOrientation(.landscapeRight)
For iPad or Mac apps, consider using the .previewLayout() modifier to fix the preview to a specific size (e.g., .sizeThatFits or an exact CGSize).
Localization
To see how your UI adapts to different languages, use the .environment(\.locale, …) modifier:
MyView()
.environment(\.locale, Locale(identifier: "ar-SA"))
Combine this with .previewDisplayName() to label each language variant.
Previews with Real Data: State, Bindings, and Mock Data
Many SwiftUI views depend on state, observed objects, or environment values. Previews can supply dummy data so you can verify behavior without running the app.
Simple State and Bindings
If your view uses @State or @Binding, you can create a wrapper view in the preview that owns the state and passes it down:
#Preview {
@State var text = "Hello"
return MyTextFieldView(text: $text)
}
The @State property wrapper is legal inside a preview because the macro expands to a view builder context.
Observed Objects and Environment Objects
For views that rely on @ObservedObject or @EnvironmentObject, inject a mock object:
class MockViewModel: ObservableObject {
@Published var title = "Mock Title"
@Published var items = ["A", "B", "C"]
}
#Preview {
let mock = MockViewModel()
MyListView()
.environmentObject(mock)
}
Using a dedicated mock class keeps your preview data separate from production code and makes it easy to test different scenarios.
Previews for Different View States
Business logic often demands different UI states: loading, empty, error, and populated. Create one preview per state. Here’s a clean pattern using an enum:
enum ViewState {
case loading, empty, populated, error
}
#Preview("Loading") {
ProfileView(state: .loading)
}
#Preview("Empty") {
ProfileView(state: .empty)
}
#Preview("Error") {
ProfileView(state: .error)
}
#Preview("Populated") {
ProfileView(state: .populated)
}
This approach changes the way you design — you naturally think about all states because each one gets its own preview.
Previews for UIKit Integration (UIViewRepresentable, UIViewControllerRepresentable)
Even if you’re bridging UIKit components, you can still preview them. Create a SwiftUI wrapper struct that conforms to UIViewRepresentable or UIViewControllerRepresentable, and then preview that wrapper.
struct MapViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView { ... }
func updateUIView(...) { ... }
}
#Preview {
MapViewWrapper()
.frame(height: 300)
}
This is incredibly useful when you’re iterating on the layout of a mixed UIKit-SwiftUI screen without needing to launch the full app.
Performance Tips: Making Previews Truly Fast
Previews are already fast because they compile only the view and its dependencies, but you can further optimize your workspace.
- Keep preview structs lean. Don’t include heavy network calls or database fetches inside previews. Use mock data or static properties.
- Use the
#Previewmacro. It compiles slightly faster than the old provider pattern because it avoids the additional struct overhead. - Disable autoplay and animations in previews. While interactive previews are great, animations that run in a loop can slow down the canvas. Use
.animation(nil)or.transaction { $0.disablesAnimations = true }in your preview if needed. - Minimize the number of simultaneous previews. Xcode runs each preview in a separate process. If you have 20 previews in one file, performance may degrade. Group related previews and comment out others while focusing on a particular variation.
- Use
#Previewwith explicit preview display names — this also helps Xcode cache the preview results per name.
Common Pitfalls and How to Avoid Them
Preview Doesn’t Update
If the canvas shows a stale version, click the Resume button or press Option+Command+P to force a refresh. Sometimes Xcode’s preview agent crashes silently — restarting Xcode usually fixes it.
Preview Compilation Error
Previews compile only the dependency graph of the view. If your view references types from outside the current module (e.g., a separate framework), you need to ensure those frameworks are built for the simulator. Check your scheme’s build order.
Previews Not Appearing for UIKit Classes
UIKit classes themselves live in .swift files but cannot be shown in the canvas unless wrapped in a SwiftUI representable. Create a wrapper struct in the same file and preview that wrapper.
Slow Preview Builds
If previews take more than 10–15 seconds to update, your view may be pulling in too much of the app’s logic. Extract data preparation into a separate mock layer. Also, close other Xcode projects and use a simulator that is already booted.
Best Practices for an Efficient Preview Workflow
- Preview one view at a time. Avoid nesting large view hierarchies inside a single preview. If you need to see a parent-child relationship, preview the parent and pass mock child data.
- Use previews to enforce design system consistency. Create a base component and preview it in all color schemes, dynamic type sizes, and orientations before integrating it into the main app.
- Keep preview data self-contained. Define sample data directly inside the preview block or as static properties in an extension. This makes the preview easy to read and maintain.
- Leverage breakpoints in previews. You can set breakpoints inside your view’s body or helper functions; the preview agent will pause execution, stepping through logic just like a full simulator run.
- Write previews early, not as an afterthought. When you create a new view, immediately scaffold a preview with representative states. This discipline often reveals missing UI states and data dependencies.
- Use preview display names. Even with the
#Previewmacro, add a string label. Xcode uses these names in the canvas and the preview list, making navigation easier.
Integrating Previews with Testing
Previews are not a replacement for unit tests or UI tests, but they complement them. Use previews to visually verify that a view renders correctly, then write unit tests for business logic and snapshot tests for visual regression. Many teams combine previews with tools like Xcode Previews and snapshot testing libraries to catch unintended visual changes automatically.
For data-driven views, consider creating a dedicated preview file that contains all the mock objects and preview configurations. This file can be excluded from the production build target, keeping your release binary clean.
Advanced Preview Techniques
Previewing Custom Shapes and Border Effects
When building custom Shapes, you can preview them in the canvas to see how they scale, rotate, or animate. Use .frame(width:height:) and different background colors to understand the shape’s bounding box.
Previewing Animations
To preview an animation, use a Timer or a @State that toggles periodically inside the preview block. For example:
#Preview {
@State var isAnimating = false
MyAnimatedView(isAnimating: isAnimating)
.onAppear { isAnimating = true }
}
Be cautious with infinite loops — they consume resources and can lock the canvas. Use a short delay or let the animation stop naturally.
Previewing Multiple Interface Styles
Beyond light and dark, Xcode supports contrast variants (high contrast). Use the .environment(\.accessibilityEnabled, true) and .environment(\.colorSchemeContrast, .increased) modifiers to simulate accessibility settings.
External Resources
- Apple’s SwiftUI Previews Documentation — official reference for all preview modifiers.
- Hacking with Swift: How to use the #Preview macro — practical examples and migration tips.
- Swift with Majid: The Art of SwiftUI Previews — deeper insights into custom preview configurations.
Conclusion
SwiftUI Previews are far more than a static thumbnail — they are a dynamic, interactive development tool that can transform your workflow. By setting up previews for every view — across devices, themes, states, and data variations — you catch visual bugs before they ever reach a simulator or a device. The result is faster iteration, higher confidence, and a better final product. Invest time early in mastering previews, and your UI code will become more resilient and more maintainable.