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?
@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