ScrollView content ignores taps near UIViewControllerRepresentable

As the title says, when a ScrollView is near a UIViewControllerRepresentable, then the ScrollView's content no longer accurately recognizes taps. In particular, taps along the leading edge (10 points-ish) are ignored.

Here's a working example:

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            MyRepresentable()
//                .allowsHitTesting(false)
            
            ScrollView {
                LazyVStack {
                    ForEach(0..<10, id: \.self) { index in
                        HStack(spacing: 0) {
                            Button {
                                print("tapped \(index)")
                            } label: {
                                Color.red
                            }
                            .frame(width: 50)
                            
                            Color.blue
                        }
                        .frame(height: 50)
                    }
                }
            }
        }
    }
}

Here's the representable and a placeholder controller:

struct MyRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> MyViewController {
        MyViewController()
    }
    
    func updateUIViewController(_ uiViewController: MyViewController, context: Context) {}
}

final class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .gray
    }
}

When you tap along the leading edge of the red buttons, the taps are ignored for about the first 10 points. But if you prevent hit testing on the representable (by un-commenting the .allowsHitTesting modifier), then the red buttons behave as expected. Also if you just remove the representable entirely, then all the buttons behave as expected.

It's as if the hit targets of the buttons are getting "pushed" over by the representable. Or is the representable simply intercepting these touches?

I've confirmed this incorrect behavior on iPad via touch and mouse. However, Apple Pencil (1st gen) and Apple Pencil Pro behave correctly - even in the presence of that UIViewControllerRepresentable. Perhaps the Pencil follows a different hit-test codepath?

Is this expected behavior? If so, then how do I use UIViewControllerRepresentable and ScrollView side-by-side?

Answered by DTS Engineer in 801355022

@Ken_D The UIViewControllerRepresentable and the content of your ScrollView don't overlap so I don't think its an issue with the Representable interfering with the hit test operations.

Try this:

Button {
    print("tapped \(index)")
} label: {
    Text("two")
        .frame(width: 500, height: 500)
        .contentShape(.rect)
}

See: https://developer.apple.com/videos/play/wwdc2023/10162/?time=992

Also, the issue in your code becomes clear when you add a border before and after the button and frame. e.g.

Button...
  .border(.red)
  .frame(...)
  .border(.blue)

It would show a tiny red rectangle (the hit testable button) within a large blue rectangle (frame). Frames are views that wrap other views and propose new sizes to their children, they don’t modify what size a child wants to be

@Ken_D The UIViewControllerRepresentable and the content of your ScrollView don't overlap so I don't think its an issue with the Representable interfering with the hit test operations.

Try this:

Button {
    print("tapped \(index)")
} label: {
    Text("two")
        .frame(width: 500, height: 500)
        .contentShape(.rect)
}

See: https://developer.apple.com/videos/play/wwdc2023/10162/?time=992

Also, the issue in your code becomes clear when you add a border before and after the button and frame. e.g.

Button...
  .border(.red)
  .frame(...)
  .border(.blue)

It would show a tiny red rectangle (the hit testable button) within a large blue rectangle (frame). Frames are views that wrap other views and propose new sizes to their children, they don’t modify what size a child wants to be

Here's another example that I hope illustrates the issue more clearly.

ZStack {
    MyRepresentable()

    ScrollView {
        VStack {
            ForEach(1...10, id: \.self) { index in
                Button {
                    print("Tapped \(index)")
                } label: {
                    Text("Button \(index)")
                }
                .buttonStyle(.borderedProminent)
            }
        }
    }
    .border(.black)
}

Using a ZStack, we place a ScrollView on top of a UIViewControllerRepresentable. We then fill the ScrollView with Button views (as .borderedProminent).

Please tap any of those buttons right on their leading or trailing edges. Nothing happens. There's a dead-zone about 10-points wide on the leading and trailing edge of each button. So you have to move your cursor or finger about 10 points into the button surface before the tap is recognized.

Now comment-out the UIViewControllerRepresentable. The buttons suddenly work as expected - even recognizing taps well outside their frames.

I'd love if anyone can shed some light on this. And if there's some silly mistake I'm making when initializing the UIViewControllerRepresentable, then apologies in advance. That's par for the course with me :)

ScrollView content ignores taps near UIViewControllerRepresentable
 
 
Q