Using SwiftUI Previews Effectively in Xcode 15+
Learn how to add and use SwiftUI #Preview blocks in Xcode 15+, understand their sandboxed architecture, and weigh the build‑time and API limitations against rapid UI iteration.
16 Jan 2026, 11:08 UTC

Problem: UI changes feel slow to verify
When iterating on a SwiftUI view, repeatedly building and running the app on a simulator or device can interrupt flow. Each launch takes seconds, and you lose the ability to see how the view looks under different device sizes, dark mode, or dynamic type settings without rebuilding.
Thesis: SwiftUI Previews give you a live, sandboxed canvas that updates as you edit code, letting you validate UI states quickly while keeping the main app target untouched.
How Previews Work
Xcode compiles preview code into a separate target that runs in a sandboxed process. The preview target depends on the same SwiftUI framework as your app, but it is isolated from file system, network, and most hardware APIs. When you edit a view file, Xcode detects the change, recompiles only the preview target, and pushes the updated UI to the canvas. This hot‑reload‑like behavior works because the preview code must be deterministic and free of side effects.
Previews are declared either with the legacy PreviewProvider protocol or, since Xcode 15, with the concise #Preview macro. Both approaches create one or more preview instances that can be configured with .previewLayout, .environment, or .previewDevice modifiers to simulate different screen sizes, orientations, or dynamic type sizes.
Worked Example: Adding a #Preview Block
Suppose you have a simple profile view:
import SwiftUI
struct ProfileView: View {
let name: String
let avatarURL: URL?
var body: some View {
VStack(spacing: 12) {
if let url = avatarURL {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image.resizable().scaledToFit().frame(width: 80, height: 80)
case .failure:
Image(systemName: "person.crop.circle")
@unknown default:
EmptyView()
}
}
.frame(width: 80, height: 80)
.clipShape(Circle())
} else {
Image(systemName: "person.crop.circle")
.font(.system(size: 80))
}
Text(name)
.font(.title2)
.bold()
}
.padding()
}
}
To add a preview that shows the view in light and dark mode, on an iPhone 14 Pro, and with a dynamic type size of .accessibilityLarge, place the following code at the bottom of the same file:
#Preview("Light mode") {
ProfileView(name: "Ada Lovelace", avatarURL: URL(string: "https://example.com/ada.jpg"))
.previewDevice("iPhone 14 Pro")
.previewLayout(.sizeThatFits)
}
#Preview("Dark mode, large text") {
ProfileView(name: "Ada Lovelace", avatarURL: URL(string: "https://example.com/ada.jpg"))
.preferredColorScheme(.dark)
.environment(\.sizeCategory, .accessibilityLarge)
.previewDevice("iPhone 14 Pro")
.previewLayout(.sizeThatFits)
}
After saving the file, Xcode’s canvas updates automatically. You can interact with the preview (e.g., toggle dark mode via the environment modifier) without launching a simulator. If you change name or avatarURL, the preview refreshes within a second, showing the new UI.
Trade‑offs and Limitations
- Build time and memory: Each preview adds a compilation target and a sandbox process. In large projects, enabling previews for many files can increase incremental build times by 10‑30 % and consume extra RAM. You can measure the impact by toggling Build Active Architecture Only in the scheme settings and comparing the
xcodebuild -showBuildTimingSummaryoutput before and after enabling previews. - Sandbox restrictions: Previews cannot read from the app’s container, make network requests, or access hardware‑dependent APIs such as location or camera. If a view relies on those services, the preview will show placeholder data or fail to render. To verify the limitation, run the same view in a simulator and compare behavior; the preview will lack the real‑world side effects.
- Determinism requirement: Preview code must not contain mutable state that changes between renders (e.g.,
Date()or random number generators). Non‑deterministic code leads to stale or flickering previews. The fix is to inject dependencies (like a date provider) and supply a fixed value in the preview. - Version requirement: The
#Previewmacro needs Swift 5.9+ and Xcode 15+. Older projects must stay withPreviewProvider.
Practical Checks and Actionable Closing
To make sure previews are helping rather than hindering your workflow:
- Enable only needed previews: In the file inspector, uncheck "Include in Previews" for files you don’t need to visualize.
- Monitor build impact: Open the Report Navigator, select a recent build, and look at the "Previews" section of the timing summary. If preview compilation dominates, consider reducing the number of concurrent previews.
- Validate sandbox limits: For any view that uses
@EnvironmentObjectfor networking or location, add a preview that supplies a mock object and confirm the UI renders; then run the same view on a simulator to see the real behavior. - Keep previews side‑effect free: Replace calls like
Date()with a injectedDateProviderprotocol and pass a fixed date in the preview block.
By treating SwiftUI Previews as a lightweight UI sandbox—useful for rapid layout and state checks but not a substitute for running on a device—you can shorten the edit‑compile‑run loop while staying aware of the extra build cost and API limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.