Kaiye
Kaiye

Reputation: 3

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.

Upvotes: 0

Views: 76

Answers (1)

Benzy Neez
Benzy Neez

Reputation: 21830

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.

  • In the original version of the code, you were attaching a sheet to every CollectionView. The reason why the sheet was sometimes failing to show may be because of conflicts due to this duplication and fragmentation.

  • In the revised version, you are attaching a sheet to the LazyHStack in every ListView. This means, there are still four .sheet modifiers in operation.

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:

  • Attach the .sheet to the outer ScrollView.
  • Remove the .id modifier from the ZStack inside the sheet.
  • Pass selectedModel as a binding to the child views.
  • Use let instead of var whenever possible.
  • You should certainly not be passing in a parameter that is received as a @State variable. State variables should always be local and private.

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)
            }
        }
    }
}

Upvotes: 0

Related Questions