Wrong View Displayed When Rapidly Switching Between Sheets

I'm experiencing an inconsistent behavior with SwiftUI sheets in my app. I have a simple view with cards and a settings button, both of which open different sheets. Here's a minimal reproducible example:

import SwiftUI

struct Card: Identifiable {
    let id = UUID()
    let text: String
}

struct ContentView: View {
    @State private var showSettings = false
    @State private var selectedCard: Card?
    
    let cards = [
        Card(text: "Card 1"),
        Card(text: "Card 2"),
        Card(text: "Card 3"),
        Card(text: "Card 4"),
        Card(text: "Card 5")
    ]
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 20) {
                    ForEach(cards) { card in
                        CardView(text: card.text)
                            .onTapGesture {
                                selectedCard = card
                            }
                    }
                }
                .padding()
            }
            .navigationTitle("My Cards")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        showSettings = true
                    }) {
                        Image(systemName: "gear")
                    }
                }
            }
            .sheet(isPresented: $showSettings) {
                SettingsView()
            }
            .sheet(item: $selectedCard) { card in
                Text("You tapped \(card.text)")
                    .padding()
            }
        }
    }
}

struct CardView: View {
    let text: String
    
    var body: some View {
        Text(text)
            .frame(maxWidth: .infinity)
            .padding()
            .background(Color.blue.opacity(0.1))
            .cornerRadius(10)
    }
}

struct SettingsView: View {
    var body: some View {
        Text("Settings")
            .navigationTitle("Settings")
    }
}

When rapidly tapping between cards and the settings button, sometimes the settings sheet displays "You tapped card X" instead of the actual settings view. This isn't due to misclicks, as the settings button and cards are far apart on the screen.

Steps to reproduce:

Is this a known issue with SwiftUI sheets? Could it be related to some kind of caching mechanism? How can I ensure that the correct sheet content is always displayed, even when rapidly switching between different sheets?

I've encountered this issue across multiple projects, so I don't believe it's specific to this particular implementation. Any insights or workarounds would be greatly appreciated!

You have to do it super fast, like opening the sheet as soon as the other one is closing. I'm also using a real device, if it matters.

Upvotes: 1

Views: 118

Answers (1)

Benzy Neez
Benzy Neez

Reputation: 21730

I couldn't reproduce the issue when running on a simulator, I probably wasn't able to switch sheets fast enough.

Anyway, it might help if there is only one .sheet modifier. This can be achieved by using an enum as the item type. For the case of a card selection, the enum can have a Card as associated value.

For this to work, the enum needs to be Identifiable. The easiest way to satisfy this is to have both the enum and Card implement Hashable too.

struct Card: Identifiable, Hashable {
    let id = UUID()
    let text: String
}

struct ContentView: View {

    enum SheetContent: Identifiable, Hashable {
        case settings
        case card(Card)

        var id: Self { self }
    }

    @State private var sheetContent: SheetContent?

    let cards = [
        // ...
    ]

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 20) {
                    ForEach(cards) { card in
                        CardView(text: card.text)
                            .onTapGesture {
                                sheetContent = .card(card)
                            }
                    }
                }
                .padding()
            }
            .navigationTitle("My Cards")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        sheetContent = .settings
                    } label: {
                        Image(systemName: "gear")
                    }
                }
            }
            .sheet(item: $sheetContent) { content in
                switch(content) {
                case .settings:
                    SettingsView()
                case .card(let card):
                    Text("You tapped \(card.text)")
                }
            }
        }
    }
}

Ps. NavigationView is deprecated, so if you don't need to support iOS 15 or earlier then suggest using a NavigationStack instead.

Upvotes: 1

Related Questions