Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI ScrollView's onTapGesture getting called for child events

Tags:

ios

swift

swiftui

I stumbled upon an event bubbling problem related to SwiftUI's ScrollView and have been able to reduce it to a short snippet of code. Please see below:

struct ContentView: View {
  var body: some View {
    ScrollView() {
      Rectangle()
      .fill(Color.red)
      .frame(width: 200, height: 200)
      .onTapGesture {
        print("Rectangle onTapGesture")
      }
    }
    .onTapGesture {
      print("ScrollView onTapGesture")
    }
  }
}

When tapping outside of the Rectangle, I see in the console:

ScrollView onTapGesture

However, when tapping the Rectangle, I see two lines being printed:

ScrollView onTapGesture

Rectangle onTapGesture

The ScrollView seems to be responding to its child's event as well... that's not supposed to happen, right? What can I do to stop this?

EDIT: just to add to the madness, these two lines don't always appear in the same order! I've seen them swapped when restarting the app without any changes to the code.

My goal was to use an onTapGesture on the ScrollView to catch "dismissing" taps i.e., taps that were not caught/handled by any of the ScrollView's children.

Thank you very much!

like image 902
Greg Sadetsky Avatar asked Jun 01 '20 21:06

Greg Sadetsky


1 Answers

Here's a straightforward solution using GeometryReader and a VStack container for your ScrollView content:

struct ContentView: View {
  var body: some View {
    GeometryReader { contentView in
      ScrollView {
        VStack {
          Rectangle()
            .fill(Color.red)
            .frame(width: 200, height: 200)
            .onTapGesture {
              print("Rectangle onTapGesture")
          }
        }
        .frame(minWidth: contentView.size.width, minHeight: contentView.size.height, alignment: .top)
        .contentShape(Rectangle())
        .onTapGesture {
          print("ScrollViewArea onTapGesture")
        }
      }
    }
  }
}

This gives you a VStack container that is always exactly the same size as its parent ScrollView because of the dynamic values we get from GeometryReader's size property.

Note that the alignment: .top on this container is there to make it behave like a normal ScrollView would, anchoring scrolling items to the top. The added bonus is that if you remove that alignment attribute your scrolling items will start from the middle of the screen—something I've found to be impossible to do before stumbling on that solution. This could be interesting UX-wise as shorter lists could make sense to be centered vertically. I digress.

Final note is the .contentShape modifier being used to make the new VStack's empty space tappable, which fixes your problem.

This idea was taken from this Hacking with Swift ScrollView effects using GeometryReader article outlining how you can push this idea to another level, transforming elements as you scroll. Very fun stuff!

like image 76
tristanlabbe Avatar answered Nov 11 '22 13:11

tristanlabbe