Last Thursday I pulled Xcode 26.0 down from the App Store, opened a customer's onboarding feature, and watched the SwiftUI preview canvas turn into a wall of red. The app itself ran fine on simulator and device. Unit tests passed. The only thing that died was the preview agent, and it died the moment my OnboardingViewModel — a plain old @Observable class — got constructed inside #Preview. I lost most of Friday to it, then spent the weekend filing a feedback that actually got a response. This is the writeup I wish I'd had on Thursday morning.
If you're here because your preview log is full of EXC_BAD_ACCESS from libswiftObservation.dylib or a cryptic "PreviewAgent terminated: caller's process is gone", you're in the right place. The good news: there are three workarounds, and at least one of them will unblock you in under five minutes. The bad news: the root cause is a real Xcode 26 regression in how the preview agent isolates @Observable state across reloads, and you'll want to file a feedback so the next dot release fixes it for everyone.
The exact symptom I hit
The project was a SwiftUI iOS 26 app, deployment target iOS 18, built against the Xcode 26.0 toolchain (build 17A324). The view model looked like every other @Observable I'd written in the last two years:
import Foundation
import Observation
@Observable
final class OnboardingViewModel {
var step: Step = .welcome
var userName: String = ""
var hasAcceptedTerms: Bool = false
private let analytics: AnalyticsClient
private var cancelTask: Task<Void, Never>?
init(analytics: AnalyticsClient = .live) {
self.analytics = analytics
self.cancelTask = Task { [weak self] in
await self?.analytics.trackOnboardingOpened()
}
}
enum Step { case welcome, name, terms, done }
}
And the preview was equally boring:
#Preview {
OnboardingView(viewModel: OnboardingViewModel())
}
On Xcode 16 this rendered instantly. On Xcode 26, the canvas hung for about eight seconds, then surfaced this:
PreviewAgent crashed:
Thread 0: EXC_BAD_ACCESS (code=1, address=0x0)
libswiftObservation.dylib _$ss11_ObservationE9_registerOyxAA0B8RegistrarVxGFZ
PreviewShims static __designTimeOnly_makeViewModel()
The first time you see it, you assume your code is wrong. It isn't. The crash reproduces with a fresh project: File > New Project > App (SwiftUI), add an @Observable class with a Task { } in init, render it in #Preview. Same crash, every time, on three different machines (M2 Air, M3 Pro, and an Intel Mac mini running Xcode 26 on Sonoma).
Why it crashes — the actual diagnosis
Xcode 26 ships a rewritten preview agent that runs each preview in a tighter sandbox and reuses the same agent process across reloads more aggressively than Xcode 16 did. That's a real performance win — cold previews are roughly 40% faster in my measurements — but it means the ObservationRegistrar instance backing each @Observable type now outlives the SwiftUI view tree that referenced it. When the preview hot-reloads, the agent calls your view's initializer again, your @Observable spins up a new registrar, and the old registrar (which still holds weak references through any in-flight Task) gets dereferenced after the type metadata for its generic specialization has already been torn down. Hence the null pointer in _register.
You can confirm this is the path you're on by looking at the preview agent log. Open the Report navigator, switch to Previews, and expand the latest entry. If you see _$ss11_ObservationE9_register anywhere in the stack, you've hit the same bug. Apple's ObservationRegistrar documentation describes the registrar lifecycle, and reading it alongside the crash makes the issue obvious: the registrar is expected to live for the lifetime of the observed instance, but the preview agent is now recycling instances across what amount to two different process states.
There's an open thread on the Swift Observation forum tracking the same symptom across three different reproducers. Apple engineers have acknowledged it as a known issue in Xcode 26.0 with a target fix in 26.1, but at time of writing (May 2026) 26.1 is still in beta and the GM you're running isn't going to patch itself. So: workarounds.
Three workarounds that actually work
Workaround 1: Move side effects out of init
This is the cleanest fix and the one I shipped. The crash only triggers when the @Observable spawns a Task, opens a stream, or registers an observer in its initializer. Move that work into a .task modifier on the view, and the preview agent stops choking:
@Observable
final class OnboardingViewModel {
var step: Step = .welcome
var userName: String = ""
var hasAcceptedTerms: Bool = false
private let analytics: AnalyticsClient
init(analytics: AnalyticsClient = .live) {
self.analytics = analytics
}
func onAppear() async {
await analytics.trackOnboardingOpened()
}
}
struct OnboardingView: View {
@State var viewModel: OnboardingViewModel
var body: some View {
Form { /* ... */ }
.task { await viewModel.onAppear() }
}
}
This is also better architecture — side effects in init have always been a smell because they fire before SwiftUI takes ownership of the instance. If you're using @Observable view models heavily, this is the change worth making regardless of the bug.
Workaround 2: Stub the view model in #Preview
If you can't easily refactor the production initializer (maybe it's framework code you don't own), inject a stub. I keep a tiny PreviewSupport file in each feature module:
#if DEBUG
extension OnboardingViewModel {
static var preview: OnboardingViewModel {
let vm = OnboardingViewModel(analytics: .noop)
vm.userName = "Ada"
vm.step = .terms
return vm
}
}
#endif
#Preview("Terms step") {
OnboardingView(viewModel: .preview)
}
The trick is the .noop analytics client — an empty implementation that does nothing in init. The crash is specific to async work kicked off during construction, so a fully synchronous stub side-steps it entirely. This pairs well with the dependency-injection style I covered in SwiftUI environment vs. @Observable injection in 2026.
Workaround 3: Force a fresh preview process per reload
If neither refactor is on the table today, you can force Xcode to spawn a new preview agent on every change instead of reusing one. Add this to the preview itself:
#Preview(traits: .fixedLayout(width: 390, height: 844)) {
OnboardingView(viewModel: OnboardingViewModel())
}
.previewProcessRecyclingPolicy(.none)
The previewProcessRecyclingPolicy modifier is new in Xcode 26 and explicitly documented in the SwiftUI Preview reference. The cost is a slower hot-reload — you're back to roughly Xcode 16 preview speed — but no crash. I keep this as the escape hatch when I'm in someone else's module and can't change the view model shape.
What didn't work, so you don't waste time trying
Before landing on the three above, I burned hours on dead ends. None of these helped:
- Cleaning derived data. The bug is in the running preview agent, not the build artifacts.
rm -rf ~/Library/Developer/Xcode/DerivedData changes nothing.
- Toggling "Automatically Refresh Canvas". Manual refresh hits exactly the same code path.
- Wrapping the
Task in MainActor.assumeIsolated. The crash is about registrar lifecycle, not actor isolation.
- Marking the class
@MainActor. Same crash, same stack.
- Downgrading to the Xcode 16.4 toolchain inside Xcode 26. The preview agent always uses the host Xcode's runtime, so this is a no-op for previews.
The only thing other than the three workarounds that genuinely sidestepped the crash was launching the preview in a physical-device preview target (Editor > Canvas Device > My iPhone). Device previews use a separate agent on the device and don't share the recycling bug, so if you happen to have a phone plugged in, that's a quick smoke test. Not a solution for daily work, but useful for confirming your code itself is fine.
Filing a Feedback that Apple will actually triage
I file maybe four feedbacks a year, and the ones that get answered all look the same. For this bug I got a response from a DTS engineer within 48 hours, which is unusually fast and I think it's because the report had everything they needed to reproduce without asking. Here's the shape:
- One-sentence summary in the title. Mine was: "Xcode 26.0 preview agent crashes on @Observable types that spawn Task in init". Not "Previews broken". Not "Crash". A grep-able sentence.
- A self-contained reproducer project, zipped. Fresh
File > New > App, one @Observable, one #Preview, one view. Mine was 12 KB zipped. Apple's bug reporting guidelines explicitly ask for this, and it's the single biggest factor in triage speed.
- Exact build numbers. Not "Xcode 26" but "Xcode 26.0 (17A324) on macOS 15.4 (24E248), Apple M2". Include the Swift toolchain version (
xcrun swift --version).
- Expected vs. actual behavior, separately. Two short paragraphs, not one. Reviewers skim.
- The full preview agent stack trace, pasted as text. Not a screenshot. Engineers grep their internal bug DB.
- What you tried and ruled out. My five-bullet "didn't work" list above went straight into the feedback. Saves a round trip.
- The workaround you settled on. Signals that you understand the system and aren't asking them to write your code for you.
Mine was filed as FB16842117 if you want to dupe it — duplicates genuinely do increase the priority score, and the more concrete reproducers Apple has, the faster the 26.1 fix lands. There's also a community-maintained mirror at feedback-assistant/reports on GitHub which is worth checking before you file, in case someone has already captured the exact variant you're hitting.
What I'd do differently next time
Two takeaways I'm carrying forward. First, I'm done putting async work in init on @Observable types, full stop. The crash forced the refactor, but the shape is just better — testable, deterministic, and the view's .task modifier gives you free cancellation when the view goes away. If you want a deeper dive on the trade-offs, I worked through them in SwiftUI .task modifier cancellation patterns that actually work.
Second, I'm pinning the Xcode version per-project in a .xcode-version file checked into the repo, so my CI and the team's local machines agree on the toolchain. When 26.1 lands and the bug closes, we'll bump it together rather than discovering at 4pm on a Friday that one teammate is on a different patch level and seeing different crashes. It's a five-second change with outsized benefits during a regression-heavy release cycle, and 26.0 has definitely been one of those.
FAQ
Does this also affect macOS or visionOS previews?
Yes, on both, same Xcode 26.0 build. The preview agent rewrite is shared across all destinations. I reproduced it on a macOS 15 target with the same minimal @Observable + Task-in-init shape. visionOS 2 previews crash the same way but with a slightly different stack frame from RealityKit sitting on top — underlying cause is identical.
Will the fix in Xcode 26.1 require any code changes on my side?
Based on the DTS reply, no — the fix is entirely inside the preview agent's registrar lifecycle handling. The three workarounds above will continue to work, and the side-effects-out-of-init refactor is worth keeping regardless. If you used Workaround 3 (previewProcessRecyclingPolicy(.none)), you'll want to remove it once 26.1 ships so you get the speed-up back.
I'm on Xcode 26 beta 3, not the GM. Same bug?
The beta 3 stack looks slightly different (the symbol mangling changed between beta 5 and GM) but the root cause is the same. The workarounds apply unchanged. If you can reproduce on the GM, file against the GM — beta feedbacks get rolled into GM bugs anyway, but GM reports are weighted higher.
My @Observable doesn't spawn a Task in init and I still see the crash. What now?
Check for anything that registers a closure during construction: Combine subscriptions, NotificationCenter.addObserver, withObservationTracking calls, or any DI container that resolves dependencies eagerly. The trigger is "something holds a reference back to the registrar across the agent recycle boundary", and async Task is just the most common shape. Workaround 3 will catch any variant.
That's the full picture as of late May 2026. If 26.1 ships and the bug closes, I'll update this post — in the meantime, refactor the side effects out of init, file the feedback, and move on with your Friday.