DEV Community

Gamya
Gamya

Posted on

The Crash That Only Happened Sometimes — A SwiftUI Bug

Summer Bug Smash: Smash Stories 🐛🛹

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.


I spent three hours debugging a SwiftUI crash that felt completely random. It wasn't random at all. It was me, a force unwrap, and a fundamental misunderstanding of when SwiftUI renders its views.

Okay gather round because I need to tell you about the three hours I spent convinced I had found a bug in SwiftUI itself. I had not found a bug in SwiftUI. I had found a bug in my own assumptions, which is both more embarrassing and more educational, and I am sharing it here so you don't have to go through the same thing.


The Setup

I was building a list-to-detail navigation flow. Classic stuff. You tap an item in a list, it navigates to a detail screen, the detail screen fetches and displays the selected item's full data. Nothing fancy. The kind of thing you've probably built or seen a dozen times.

I had a view model managing the selected item state, and because I was moving fast and feeling a little overconfident, I declared it like this:

@Published var selectedItem: Item!
Enter fullscreen mode Exit fullscreen mode

That ! at the end. That little exclamation mark. That tiny, innocent-looking, completely catastrophic punctuation.

For those who haven't encountered this particular flavour of Swift danger: that's an implicitly unwrapped optional. It means the variable can technically be nil, but you're telling Swift "trust me, it won't be." Swift says "okay" and doesn't make you check. Until it is nil. And then your app explodes.

My reasoning at the time was: by the time the detail view renders, I'll have set selectedItem. It's fine. It'll be fine.

Reader, it was not fine.


The Crime Scene

The crash started showing up in testing. But here's the thing that made it so maddening: it didn't happen every time.

Sometimes I'd tap an item, the detail screen would appear, everything would be perfect. Sometimes I'd tap an item and the app would crash immediately with a cryptic EXC_BAD_INSTRUCTION on a line inside the view's body. No useful stack trace. No clear reproduction steps. Just: sometimes crash, sometimes not.

My first instinct was device-specific. Tested on different simulators. Nope.

My second instinct was data-specific. Tested with different items. Nope.

My third instinct, I am slightly embarrassed to admit, was that SwiftUI was doing something weird with navigation timing. I spent a genuinely non-trivial amount of time reading about NavigationStack rendering behaviour and convincing myself the framework had an edge case.

It did not have an edge case. I had an edge case.


The Investigation

What finally broke it open was slowing down and actually tracing the execution order instead of staring at the crash line.

Here's what I thought was happening:

  1. User taps item
  2. selectedItem gets set in the view model
  3. SwiftUI navigates to detail view
  4. Detail view renders using selectedItem

Here's what was actually happening:

  1. User taps item
  2. SwiftUI begins evaluating and rendering the destination view
  3. selectedItem is still nil during this initial render pass
  4. The force unwrap fires
  5. Crash

SwiftUI doesn't wait politely for your async state to be ready before it starts rendering. It evaluates the view body, and if your view body contains a force-unwrapped optional that isn't set yet, it will crash during that evaluation. The "sometimes" quality of the bug came from timing — on faster devices or with cached data, selectedItem was set fast enough that the render pass caught it in time. On slower paths, it didn't.

The crash wasn't random. It was a race condition I had created with an exclamation mark.


The Fix

Once I understood the actual problem, the fix was straightforward. I replaced the implicitly unwrapped optional with a safe optional:

// Before — ticking time bomb
@Published var selectedItem: Item!

// After — honest about what this can be
@Published var selectedItem: Item?
Enter fullscreen mode Exit fullscreen mode

And updated the SwiftUI view to handle every state the data could actually be in:

// Before — assumes selectedItem is always there
var body: some View {
    ItemDetailView(item: viewModel.selectedItem)
}

// After — handles every intermediate state explicitly
var body: some View {
    if viewModel.isLoading {
        ProgressView()
    } else if let item = viewModel.selectedItem {
        ItemDetailView(item: item)
    } else {
        Text("Something went wrong.")
    }
}
Enter fullscreen mode Exit fullscreen mode

The crash vanished immediately. The fix took about ten minutes. The debugging took three hours.


What I Actually Learned

The technical lesson is: in SwiftUI, your UI must be able to render safely for every intermediate state. SwiftUI is declarative and reactive — it can and will evaluate your view body at any point, including before your async operations complete. If your view assumes state that hasn't arrived yet, it will fail exactly when that assumption turns out to be wrong.

The deeper lesson is: implicitly unwrapped optionals are almost never the right call in a SwiftUI view model. They exist for situations where initialization order makes it genuinely impossible to set a value at declaration time — like @IBOutlet connections in UIKit. They are not a way to skip handling states you find inconvenient. Every time you write ! to avoid dealing with an optional, you're writing a crash that just hasn't happened yet.

The thing that still makes me a little sheepish: I knew this, in the abstract. I'd read about IUOs. I understood optionals. I just got lazy in a moment of "this will clearly be set by the time it matters."

SwiftUI disagreed. SwiftUI was right.


AI tools were used to help with grammar and structure.

Top comments (0)