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
preselectedTransactionTypeis notnil. - The value is passed directly into the sheet’s content closure as
type. - When the sheet is dismissed, SwiftUI automatically sets
preselectedTransactionTypeback tonil.
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#
| Approach | Sheet Triggered By | Value Passed to Sheet | Risk of Outdated Value? |
|---|---|---|---|
.sheet(isPresented:) | Boolean | Must read from state | Yes |
.sheet(item:) | Optional value | Passed as parameter | No |
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!
