Skip to content

Instantly share code, notes, and snippets.

@ole
Last active January 11, 2023 00:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ole/7dfad1b91a0bf7185c6ad6ff37ab8c1b to your computer and use it in GitHub Desktop.
Save ole/7dfad1b91a0bf7185c6ad6ff37ab8c1b to your computer and use it in GitHub Desktop.
SwiftUI: .clipped() doesn’t limit hit testing to the visible area. This is the sample code for https://oleb.net/2022/clipped-hit-testing/
import SwiftUI
struct ContentView: View {
@State private var buttonTapCount: Int = 0
@State private var rectTapCount: Int = 0
@State private var isClippingDisabled: Bool = false
@State private var activateContentShape: Bool = false
var body: some View {
VStack(spacing: 40) {
VStack {
Toggle("Show unclipped square", isOn: $isClippingDisabled)
Toggle("Limit hit testing with `.contentShape()`", isOn: $activateContentShape)
}
Grid(horizontalSpacing: 24) {
GridRow {
Text("Button")
Text("Square")
}
GridRow {
Text("\(buttonTapCount)")
Text("\(rectTapCount)")
}
.font(.largeTitle.bold().monospacedDigit())
}
VStack {
Button("You can't tap me!") {
buttonTapCount += 1
}
.buttonStyle(.borderedProminent)
ZStack {
if activateContentShape {
clippedRect
.contentShape(Rectangle())
} else {
clippedRect
}
}
.onTapGesture {
rectTapCount += 1
}
.background {
if isClippingDisabled {
rect.opacity(0.3)
}
}
}
Text("""
The orange square is actually 300×300 pt, but is clipped to 100×100.
`.clipped()` doesn’t limit hit testing to the visible area. This is why you can’t tap the button.
""")
.multilineTextAlignment(.leading)
}
.padding()
}
@ViewBuilder private var rect: some View {
Rectangle()
.fill(.orange.gradient)
.frame(width: 300, height: 300)
}
@ViewBuilder private var clippedRect: some View {
rect
.frame(width: 100, height: 100)
.clipped()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@AsceticMonk
Copy link

I tested this sample code, but I see that even when contentShape is applied, you can still tap outside of the clipped area (not the original 300x300 rect, but slightly outside the 100x100 rect). Do you know why?

@ole
Copy link
Author

ole commented Jan 9, 2023

I tested this sample code, but I see that even when contentShape is applied, you can still tap outside of the clipped area (not the original 300x300 rect, but slightly outside the 100x100 rect). Do you know why?

@AsceticMonk Good observation, I hadn't noticed this. I can reproduce this on iOS, but not on in a macOS app. Can you confirm? This makes me think that this could be a heuristic that SwiftUI employs on iOS to correct for fingers being imprecise. It looks like a near miss on a tap target is still counted as a hit as long as this doesn't interfere with any other nearby hit testing targets. If you place another rectangle right next to the clipped one and give that new shape also an .onTapGesture handler, SwiftUI won't allow "outside taps" on that edge anymore. Does this make sense to you?

@AsceticMonk
Copy link

I think your explanation makes sense, given that on iOS tapping is done with fingers. I did experiment with multiple rectangles next to each other, with very small spacing, and it seems outside taps are still allowed. In the space that 2 rectangles share, outside tapping area is shared, and divided by proximity. Of course, I can be very precise on simulator with cursor, but with finger one probably would never notice this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment