SwiftUI Previews are magical when they work. When they don't,
staring at "Preview crashed" with no clue why is one of the
most frustrating things in iOS development.
After debugging thousands of preview crashes across multiple
production apps, here are the 6 techniques I reach for every time.
Why Do SwiftUI Previews Break?
Previews run in a sandboxed environment separate from your app.
They have stricter resource limits, no access to your app's
environment by default, and their own compiler pipeline.
Once you internalize this, most failures become predictable.
Four root causes cover 90% of cases:
- Compile-time errors (one broken file cascades across the project)
- Runtime errors (force unwraps, nil crashes)
- Missing environment dependencies (environmentObject not injected)
- Complexity/timeout (view does too much, exceeds preview's time limit)
Technique 1 — Build explicitly first
Hit Cmd+B before anything else. The preview compiler sometimes
fails silently while your main target builds fine. Check the build
output for errors in files you weren't even editing.
Technique 2 — Self._printChanges()
This underused API tells you exactly why SwiftUI re-evaluated
your view's body:
var body: some View {
let _ = Self._printChanges()
// your view...
}
The console will log which @State or @ObservedObject property
changed and triggered the re-render. Invaluable for infinite
re-render loops.
Technique 3 — Use Logger, not print()
import OSLog
struct ProfileView: View {
private let logger = Logger(
subsystem: "com.yourapp",
category: "ProfileView"
)
var body: some View {
let _ = logger.debug("body evaluated")
// your view...
}
}
Logger is type-safe, shows up in Console.app with filtering,
and gets stripped from release builds automatically.
Technique 4 — Isolate by binary search
Comment out half your view body. If preview works → problem is
in the commented half. Still crashes → problem is in the remaining
half. Keep halving until you find the exact line.
If you can't isolate a view without breaking everything,
your components are too tightly coupled. That's the real bug.
Technique 5 — Inject mock dependencies
Never make real network calls from previews. Use protocol-based
injection so you can swap in a mock:
protocol DataFetching {
func fetchUsers() async throws -> [User]
}
class MockDataFetcher: DataFetching {
func fetchUsers() async throws -> [User] {
[User(id: 1, name: "Mrugesh Tank", email: "m@test.com")]
}
}
#Preview {
UserListView(dataFetcher: MockDataFetcher())
}
Technique 6 — Centralize preview mock data
Instead of scattering mock objects across every preview block,
create a single PreviewHelpers.swift:
#if DEBUG
enum PreviewData {
static let user = User(id: 1, name: "Test User", role: .admin)
static let users = [user, User(id: 2, name: "Jane", role: .member)]
}
#endif
Your previews become clean and consistent:
#Preview { ProfileView(user: PreviewData.user) }
The Bigger Lesson
Views that preview reliably are almost always well-architected views.
Preview instability is usually a symptom of tight coupling,
missing dependency injection, or views doing too much.
The path to stable previews is the same as the path to
maintainable, testable SwiftUI code.
I wrote a more detailed version of this with additional
patterns on my blog:
👉 Full article on idiots With iOS
Top comments (0)