swiftuiscrollviewhittest

Disable Content but Enable Scrolling


I am attempting to attach a modifier to a ScrollView that still allows scrolling and disables all the underlying buttons:

struct ContentView1: View {
    var body: some View {
        ScrollViewButtonDisableView()
            .modifier(ScrollHitModifier())
    }
}

struct ScrollViewButtonDisableView: View {
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0...100, id: \.self) { buttonNumber in
                    Button("Press \(buttonNumber)") {
                        print("Button number \(buttonNumber) pressed")
                    }
                    .buttonStyle(.borderedProminent)
                    .font(.title)
                    .padding()
                }
            }
        }
    }
}

struct ScrollHitModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .allowsHitTesting(false)
    }
}

Unfortunately the result is that both the buttons and the scrolling do not work any more. I still want to be able to scroll.

I know that I can add the modifier ScrollHitModifier() to the VStack in ScrollViewButtonDisableView, but that defeats the purpose of what I am looking for. I would like to add a modifier to the overall struct and not have to place it into the struct. Is there any way to attach an .allowsHitTesting modifier to the ScrollViewButtonDisableView and still allow scrolling?


Solution

  • You could add a dummy TapGesture as a .highPriorityGesture to the ScrollView. This blocks tap gestures on the content inside the ScrollView, but still allows scrolling to be performed.

    EDIT Adding .including: .gesture seems to help to prevent gestures from reaching the subviews (ref. comments). The documentation for this GestureMask states:

    Enable the added gesture but disable all gestures in the subview hierarchy.

    ...which is just what's needed!

    struct ScrollHitModifier: ViewModifier {
        func body(content: Content) -> some View {
            content
                .highPriorityGesture(
                    TapGesture(),
                    including: .gesture
                )
        }
    }