SwiftUI Bug in Modal Presentation Using Boolean State
Overview
In SwiftUI, developers often use boolean @State
properties to control the presentation of modal panes and full-cover views. However, a known bug can occur when presenting these views: sometimes, the data passed to the detail view is not initialized correctly. This issue arises when using a boolean @State
to trigger the presentation. Sometimes, this can lead to a modal view being displayed correctly only the second time it is invoked, misleading you into thinking there may be a race condition in your code.
The Problem
When you use a boolean @State
property to present a modal or full cover view, SwiftUI may fail to initialize the data being passed to the detail view. This typically happens because the boolean state change triggers the view presentation before the data is correctly set up. As a result, the detail view may receive nil or uninitialized data, leading to runtime errors or unexpected behavior.
Example Scenario
Consider the following example where a boolean @State
property is used to present a detail view:
struct ContentView: View {
@State private var showDetail = false
@State private var selectedItem: Item?
var body: some View {
VStack {
Button(action: {
selectedItem = Item(id: 1, name: "Sample Item")
print(selectedItem as Any)
showDetail.toggle()
}) {
Text("Show detail")
}
}
.fullScreenCover(isPresented: $showDetail) {
if let item = selectedItem {
DetailView(item: item)
} else {
Text("Error: item is nil")
.foregroundStyle(Color.red)
.bold()
}
}
}
}
struct DetailView: View {
var item: Item
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Item: \(item.name)")
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Dismiss")
}
}
}
}
#Preview {
ContentView()
}
In this code selectedItem
will not be properly initialized when showDetail
is toggled, leading to the detail view receiving nil data. I have just tested this with the current version of Xcode, v15.4, running on Apple Silicon.
Interestingly, place a breakpoint or observe the print result on the console. You will see that the selectedItem variable has the correct value just before triggering the visualization of the modal view, but inside the modal closure, selectedItem is nil.
A more reliable approach is to directly pass the item to the detail view, avoiding using a boolean @State
property to control the presentation. Doing this ensures the data is always correctly initialized before the view is presented.
Here’s how you can modify the example to pass the item directly:
import SwiftUI
struct ContentView: View {
@State private var selectedItem: Item?
var body: some View {
VStack {
Button("Show Detail") {
selectedItem = Item(id: 1, name: "Sample Item")
}
}
.fullScreenCover(item: $selectedItem) { item in
DetailView(item: item)
}
}
}
struct DetailView: View {
var item: Item
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Item: \(item.name)")
Button("Dismiss") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct Item: Identifiable {
let id: Int
let name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In this revised example, the fullScreenCover
modifier uses the item binding directly. The detail view will only be presented when selectedItem
non-nil, ensuring the data is always initialized before the presentation.
Nota Bene: Do not waste your time thinking this problem is a race condition you can address with your code. It is not. The best way to avoid this issue is not to control the display of a modal view with a @State boolean.
Conclusion
Using a boolean @State
Controlling modal and full-cover presentations in SwiftUI can lead to issues with data initialization. A more robust approach is to pass the item directly, ensuring that the detail view always receives properly initialized data.
If you want to learn more about SwiftUI, this is the book to get you started:
Amazon link- https://packt.link/euBNo
Packt Website Link- https://packt.link/R3dQ9