Last active
January 9, 2025 10:50
-
-
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 hidden or 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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.