TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

TabView with ScrollView in sheet disables scroll down to dismiss

Forums > SwiftUI

I'm currently working on a SwiftUI project, and I've encountered an issue with a TabView inside a sheet. It seems that when I add ScrollView instances within the TabView, the ability to scroll down to dismiss the sheet is disabled.

struct ContentView: View {
  var body: some View {
    VStack {}
      .sheet(isPresented: .constant(true)) {
        TabView {
          ScrollView(.vertical) {
            ForEach(0..<100) { i in
              Text("Item \(i)")
                .containerRelativeFrame(.horizontal)
            }
          }
          .tag(0)

          ScrollView(.vertical) {
            ForEach(0..<100) { i in
              Text("Item \(i)")
                .containerRelativeFrame(.horizontal)
            }
          }
          .tag(1)
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
      }
  }
}

In the above code, I have a ContentView that presents a sheet containing a TabView with two ScrollView instances. The issue is that when I open the sheet, I cannot scroll down to dismiss it. Instead, the scrolling is trapped within the ScrollView.

If I remove the TabView then I can drag down to dismiss sheet from the ScrollView.

Is there a way to allow scrolling down to dismiss the sheet while retaining the scroll functionality within TabView and ScrollView?

3      

Hi! You may want to add some padding to TabView so that touch gestures do not overlap each other. Below I have added some padding to TabView and you can dismiss the sheet as usual...

struct ContentView: View {
    var body: some View {
        VStack {}
            .sheet(isPresented: .constant(true)) {
                TabView {
                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                .containerRelativeFrame(.horizontal)
                        }
                    }
                    .tag(0)

                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                .containerRelativeFrame(.horizontal)
                        }
                    }
                    .tag(1)
                }
                .padding() // <- add some padding
                .tabViewStyle(.page(indexDisplayMode: .never))
            }
    }
}

3      

@ygeras, no. I want to dismiss the sheet from inside the ScrollView, not from the padding. As I mentioned, if you delete TabView and keep only the ScrollView, you can do it, but with the TabView, you can't.

3      

Just think this way. You placed following modifier to Text - .containerRelativeFrame(.horizontal) and what it does it takes all available space for that row on the screen widthwise. And that TextView now is actually working as scroll taking up all the screen. Removing that modifier makes your Scroll as narrow as the width of the TextView and you can dismiss the sheet. So obviously it is not the blame of the TabView. Now imaging youself as an iOS system - how on earth do you know what a user wants you to do? Scroll or dismiss???? You placed the layer on top which is a scroll and below that is the layer that can dismiss. What would you do in that case? Maybe it is reasonable to place some kind of indicator on top that hints the user: "Hey, use me to drag that view down to dismiss it?"

.sheet(isPresented: .constant(true)) {
                TabView {
                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                .containerRelativeFrame(.horizontal)
                        }
                    }
                    .tag(0)

                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                .containerRelativeFrame(.horizontal)
                        }
                    }
                    .tag(1)
                }
                .tabViewStyle(.page(indexDisplayMode: .never))
                .presentationDragIndicator(.visible) // <- Drag indicator
            }

or you can go ahead and write logic with complex gestures that would recognize what you want do to.

3      

As I already said, just remove the TabView and all will work as expected. Even with ScrollView. This is the native logic of iOS, and it's how many native apps work. So it's just a bug and I am looking for a solution.

struct ContentView: View {
  var body: some View {
    VStack {}
      .sheet(isPresented: .constant(true)) {
        ScrollView(.vertical) {
          ForEach(0..<100) { i in
            Text("Item \(i)")
              .containerRelativeFrame(.horizontal)
          }
        }

      }
  }
}

3      

It is not a bug, it is a contradictory gestures you are trying to use. TabView utilize swipe left and right gesture recognition and your dismiss also uses gesture recognizer to swipe it down, besides scroll also uses gesture recognizer to utilize its functions.

This is the native logic of iOS, and it's how many native apps work.

.containerRelativeFrame(.horizontal) is as of iOS17. How many apps are there that can use this modifier? )))

iOS devs cannot predict all the cases software developers are going to implement their way, and if you use contradictory actions it is not a bug. We do not see all the actions TabView and ScrollViews are utilizing for gesture recognition, but here it is obvious that those of tab view style and scroll view and sheet are in contradiction. In any case should you find a solution, you are more than welcome to post it here if no answer will be posted from someone on the forum.

3      

To further support my point, and maybe this helps you find your way to make some progress tackling the challenge, see the code below and maybe play with that to see what is happening.

struct ContentView: View {
    @State private var dismissNotActive = true

    var body: some View {
        VStack {}
            .sheet(isPresented: .constant(true)) {
                TabView {
                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                .containerRelativeFrame(.horizontal)
                                .border(Color.red)
                        }
                    }
                    .allowsHitTesting(dismissNotActive) // this will control if scroll gestures are recognizable
                    .onTapGesture(count: 3, perform: { // tap three times to switch off scroll gesture layer recognition and make dismiss layer recognition available
                        dismissNotActive = false
                    })
                    .tag(0)

                    ScrollView(.vertical) {
                        ForEach(0..<100) { i in
                            Text("Item \(i)")
                                // .containerRelativeFrame(.horizontal) // remove that
                                .border(Color.red)
                                // now scrolling only within text borders is available
                                // outside text borders you can access dismiss recognition.
                        }
                    }
                    .tag(1)
                }
                .tabViewStyle(.page(indexDisplayMode: .never))
            }
    }
}

3      

OMG! The containerRelativeFrame is just an example. Remove it if you want and nothing will change.

Also, as you said, TabView uses left and right swipe gesture recognition, so it shouldn't affect ScrollView's up and down gesture recognition, so it's definitely a bug.

3      

Good luck! I am not here to persaude you what to do. The way you answer seems like people here are obliged to provide right answers :)))) Besides removing containerRelativeFrame changes a lot as you can dismiss the sheet outside text if you did not notice that.

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.