Last active
January 11, 2023 00:57
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
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
@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?