Search code examples
swiftforeachswiftuicrashswiftui-sheet

SwiftUI using ForEach and onTapGesture to update selected element causing crash


I have some code that I am using, the idea is that the user selects a card they like and a sheet is presented to show more information about that card. The problem is, if I don't have the Text(selectedCard?.name ?? "Not selected") line, it crashes. I was just wondering what I don't understand about Swift/SwiftUI that is making this happen and how I could fix it (as I don't want it to say what card they have selected).

It is where the sheet comes to be presented that it crashes as it finds a nil value for selectedCard but selectedCard should not be nil when that runs. If I add the seemingly unrelated line to display which card has been selected, the value is set fine and it does not crash.

Full code:

import SwiftUI

struct CardsView: View {
    
    @State private var cardlist = [Cards]()
    @State private var showingSheet = false
    @State private var selectedCard: Cards?
    
    let columns = [
        GridItem(.adaptive(minimum: 120))
    ]
    
    var body: some View {
        NavigationView() {
            ScrollView (.vertical, showsIndicators: false) {
                Text("View our full collection of cards below:")
                    .padding(.leading, 20)
                    .frame(maxWidth: .infinity, alignment: .leading)
                
                Text(selectedCard?.name ?? "Not selected") // This line stops it from crashing
                
                    LazyVGrid(columns: columns, spacing: 10) {
                        ForEach(cardlist, id: \.name) { thecard in
                            CardItem(card: thecard)
                                .onTapGesture {
                                    selectedCard = thecard
                                    showingSheet.toggle()
                                }
                        }
                        
                    }
                    .padding([.leading, .trailing])
                
                Spacer()
            }
            
            .navigationTitle("Cards")
        }
        
        .sheet(isPresented: $showingSheet) {
            SpecificCardView(card: selectedCard!)
        }
        
        .onAppear() {
            loadCards()
        }
        
    }
    
    func loadCards() {
        ...
    }
    
}

Solution

  • Your problem is likely due to the view diffing algorithm having a different outcome if you have the text value present, since that would be a difference in the root view. Since you have two pieces of state that represent the sheet, and you are force unwrapping one of them, you're leaving yourself open to danger.

    You should get rid of showingSheet, then use the item binding of sheet instead. Make Cards conform to Identifiable, then rewrite the sheet modifier as:

    .sheet(item: $selectedCard) { SpecificCardView(card: $0) } 
    

    This will also reset your selected card to nil when the sheet is dismissed.