Skip to main content
Featured image for SwiftUI Sheet State Pitfall: Why Is My State Nil? (And How to Fix It)

SwiftUI Sheet State Pitfall: Why Is My State Nil? (And How to Fix It)

·390 words·2 mins

The Problem
#

Recently, while working on my AllowanceIQ app, I ran into a classic SwiftUI gotcha that I think a lot of folks will recognize. I had a view with a button that, when tapped, should open a sheet and pass in a value (in my case, a TransactionType). But sometimes, the value I passed in was nil—even though I’d just set it!

Here’s what my code looked like:

@State private var preselectedTransactionType: TransactionType? = nil
@State private var showingTransactionEntry = false

Button("Record Purchase") {
    preselectedTransactionType = .purchase
    showingTransactionEntry = true
}

.sheet(isPresented: $showingTransactionEntry) {
    TransactionEntryView(preselectedTransactionType: preselectedTransactionType)
}

Looks fine, right? But when I tapped the button, preselectedTransactionType was sometimes nil inside the sheet. What gives?


The SwiftUI State Update Race
#

The culprit is how SwiftUI batches state updates. When you set two state variables in a row, like this:

preselectedTransactionType = .purchase
showingTransactionEntry = true

SwiftUI doesn’t guarantee the first update is visible to the rest of your view before the second one triggers the sheet. So, the sheet can open while preselectedTransactionType is still nil.


The Solution: .sheet(item:) to the Rescue
#

After some head-scratching (and a few “why is this nil?!” print statements), I remembered that SwiftUI has a better way: .sheet(item:).

Here’s how you use it:

@State private var preselectedTransactionType: TransactionType?

Button("Record Purchase") {
    preselectedTransactionType = .purchase
}

.sheet(item: $preselectedTransactionType) { type in
    TransactionEntryView(preselectedTransactionType: type)
}

What’s happening here?

  • The sheet is only presented when preselectedTransactionType is not nil.
  • The value is passed directly into the sheet’s content closure as type.
  • When the sheet is dismissed, SwiftUI automatically sets preselectedTransactionType back to nil.

No more race conditions. No more nils. Just clean, reliable state.


Why This Works
#

  • .sheet(item:) is designed for exactly this use case: presenting a sheet when you have a value, and passing that value in.
  • It guarantees the value is always up-to-date when the sheet appears.
  • It’s less error-prone and more readable than juggling multiple state variables.

Summary Table
#

ApproachSheet Triggered ByValue Passed to SheetRisk of Outdated Value?
.sheet(isPresented:)BooleanMust read from stateYes
.sheet(item:)Optional valuePassed as parameterNo

TL;DR
#

If you ever find yourself passing data into a SwiftUI sheet and sometimes getting nil, switch to .sheet(item:). It’s the idiomatic, bug-free way to present sheets with data in SwiftUI.


Happy coding!