swiftuser-interfaceswiftuimodalviewcontrolleruser-interaction

multiple .sheet() presentation : Integration on fast dismiss/present


I'm trying to build a view similar to what we see in the home screen of the Netflix iOS app, with multiple rows of cells.

Here's the structure of my views:

The issue I'm facing is with the sheet presentations. From the main view, if I quickly show the detail view of item 1 by tapping on the Collection 1 rectangle, dismiss it by dragging down, and then immediately try to show the detail view of an item 2 by tapping on the Collection 2, the detail view for Collection 2 doesn't show up. If I keep tapping on different items in various collections, at some point, one of the detail views will appear. After dismissing it, all the previously attempted detail views start appearing one after the other, right after each dismiss.

This behavior sometimes happens even if I don't click quickly.

To debug it I rebuild a simpler version of it and tried to replicate the issue, here is the code :

Item struct

struct CollectionModel: Identifiable {
    let id: UUID = UUID()
    let randomNumb: Int = Int.random(in: 1...10)
    
    static var mockArray: [CollectionModel] {
        [
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel(),
            CollectionModel()
        ]
    }
}

MainView()

struct ContentView: View {

    var body: some View {
        
        ScrollView(.vertical, showsIndicators: false) {
            LazyVStack {
                ListView()
                ListView()
                ListView()
                ListView()
            }
        }.padding(.horizontal, 2)
    }
}

ListView()

struct ListView: View {
    
    var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack{
                ForEach(listOfCollectionModel) { model in
                    CollectionView(collectionModel: model)
                }
            }.padding(.horizontal)
        }
    }
    
}

CollectionView()

struct CollectionView: View {
    
    @State var collectionModel: CollectionModel
    @State var showDetails: Bool = false
    
    var body: some View {
        Text("\(collectionModel.randomNumb)")
            .padding()
            .frame(width: 200, height: 250)
            .background(.red.gradient)
            .clipShape(RoundedRectangle(cornerRadius: 15))
            .font(.title)
            .bold()
            .foregroundStyle(.white)
            .onTapGesture {
                showDetails = true
            }
            .sheet(isPresented: $showDetails){
                ZStack{
                    Color.red
                        .ignoresSafeArea()
                    Text("\(collectionModel.randomNumb)")
                }
            }
    }
    
}

After watching some tutorial on .sheet() it appears that maybe using .sheet(item) was a better approche. So I tried it as follow :

ListView()

struct ListView: View {
    
    var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
    @State var selectedModel: CollectionModel? = nil
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack{
                ForEach(listOfCollectionModel) { model in
                    CollectionView(collectionModel: model)
                        .onTapGesture {
                            selectedModel = model
                        }
                }
            }.padding(.horizontal)
                .sheet(item: $selectedModel) { model in
                    ZStack {
                        Color.black
                            .ignoresSafeArea()
                        Text("\(model.randomNumb)")
                            .font(.title)
                            .bold()
                            .foregroundStyle(.white)
                    }
                }
        }
    }
    
}

CollectionView()

struct CollectionView: View {
    
    @State var collectionModel: CollectionModel
    
    var body: some View {
        Text("\(collectionModel.randomNumb)")
            .padding()
            .frame(width: 200, height: 250)
            .background(.red.gradient)
            .clipShape(RoundedRectangle(cornerRadius: 15))
            .font(.title)
            .bold()
            .foregroundStyle(.white)
    }
    
}

Now the sheet is well presented, but the contents might sometimes be the one from the previously selected content. To avoid this issue I used .id(sheetID) as follow :

ListView()

struct ListView: View {
    
    var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
    @State var selectedModel: CollectionModel? = nil
    @State var sheetID: UUID = UUID()
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack{
                ForEach(listOfCollectionModel) { model in
                    CollectionView(collectionModel: model)
                        .onTapGesture {
                            selectedModel = model
                            sheetID = UUID()
                        }
                }
            }.padding(.horizontal)
                .sheet(item: $selectedModel) { model in
                    ZStack {
                        Color.black
                            .ignoresSafeArea()
                        Text("\(model.randomNumb)")
                            .font(.title)
                            .bold()
                            .foregroundStyle(.white)
                    }.id(sheetID)
                }
        }
    }
    
}

It now feel like it is working but I don't know... I really feel like I did things wrong in the beginning, and now I'm correcting my initial mistake (which I don't know what it is) little by little by adding layers of unnecessary fixes.

Do you guys have any thought about it ? Thanks for your help.


Reply from @Benzy Neez is working well.


Solution

  • A sheet is shown over all other content, so it is often a good idea to attach the sheet to a view that is permanently present, such as a parent container.

    It may also be significant, that the .sheet modifiers are attached to views that are inside a container with lazy loading (the ListView are nested inside a LazyVStack). This may be a cause of issue too.

    I would suggest these changes:

    Here is an updated version with these changes applied:

    struct CollectionView: View {
        let collectionModel: CollectionModel
        var body: some View {
            Text("\(collectionModel.randomNumb)")
                // ...
        }
    }
    
    struct ListView: View {
        let listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
        @Binding var selectedModel: CollectionModel?
    
        var body: some View {
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack{
                    // ...
                }
                .padding(.horizontal)
                // .sheet removed
            }
        }
    }
    
    struct ContentView: View {
        @State private var selectedModel: CollectionModel? = nil
    
        var body: some View {
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack {
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                }
            }
            .padding(.horizontal, 2)
            .sheet(item: $selectedModel) { model in
                ZStack {
                    // ...
                }
            }
        }
    }
    

    I would expect this to work reliably.


    EDIT You said in your comment that you tried the changes and also moved the tap gesture to the underlying CollectionView. That's fine, if you pass on selectedModel as a binding.

    It works for me that way. Here is the complete code, which you can just copy/paste to test/compare:

    struct CollectionView: View {
        let collectionModel: CollectionModel
        @Binding var selectedModel: CollectionModel?
    
        var body: some View {
            Text("\(collectionModel.randomNumb)")
                .padding()
                .frame(width: 200, height: 250)
                .background(.red.gradient)
                .clipShape(RoundedRectangle(cornerRadius: 15))
                .font(.title)
                .bold()
                .foregroundStyle(.white)
                .onTapGesture {
                    selectedModel = collectionModel
                }
        }
    }
    
    struct ListView: View {
        let listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
        @Binding var selectedModel: CollectionModel?
    
        var body: some View {
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack{
                    ForEach(listOfCollectionModel) { model in
                        CollectionView(collectionModel: model, selectedModel: $selectedModel)
                    }
                }
                .padding(.horizontal)
            }
        }
    }
    
    struct ContentView: View {
        @State private var selectedModel: CollectionModel? = nil
    
        var body: some View {
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack {
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                    ListView(selectedModel: $selectedModel)
                }
            }
            .padding(.horizontal, 2)
            .sheet(item: $selectedModel) { model in
                ZStack {
                    Color.black
                        .ignoresSafeArea()
                    Text("\(model.randomNumb)")
                        .font(.title)
                        .bold()
                        .foregroundStyle(.white)
                }
            }
        }
    }