SwiftUI Application development

A nasty little bug in SwitUI

    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