Asked 1 month ago by StellarScientist758
How can I fix incorrect tap detection on rounded rectangle images in SwiftUI?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by StellarScientist758
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I’m displaying images as rounded rectangles in a horizontal scroll view, and tapping an image should open it in full screen. However, when I tap the red part of Image A (which has two shades), the app mistakenly opens the wider Image B. Tapping the yellow part of Image A correctly opens Image A.
This issue appears to be related to using image.resizable().aspectRatio(contentMode: .fill)
, which fills the view and may extend the hit-test area beyond the rounded bounds. When I use .aspectRatio(contentMode: .fit)
, the tap behavior is correct, but I lose the rounded rectangle appearance.
Below is my executable sample code:
SWIFTimport SwiftUI struct Foo { var title: String var url: String var image: Image? init(title: String, url: String, image: Image? = nil) { self.title = title self.url = url self.image = image } } struct ContentViewA: View { @State private var data = [ Foo(title: "Image A", url: "https://www.shutterstock.com/image-illustration/two-shades-color-background-mix-260nw-2340299851.jpg", image: nil), Foo(title: "Image B", url: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Sydney_Harbour_Bridge_night.jpg/800px-Sydney_Harbour_Bridge_night.jpg", image: nil) // Foo(title: "Image B", url: "https://www.shutterstock.com/image-photo/ultra-wide-photo-mountains-river-260nw-1755037052.jpg", image: nil) /// There are more images in the array in real code. ] var body: some View { ZStack { Color.black.opacity(0.7).ignoresSafeArea() VStack { ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .center, spacing: 10) { ForEach(Array(data.enumerated()), id: \ .offset) { index, item in if let urlObject = URL(string: item.url) { AsyncImage(url: urlObject, scale: 1.0, transaction: Transaction(animation: .spring(response: 0.5, dampingFraction: 0.65, blendDuration: 0.025)), content: { renderPhoto(phase: $0, item: item, index: index) }) } else { /// Note: Shows placeholder view EmptyView() } } } .padding(.leading, 0) .padding(.trailing, 16) .frame(maxWidth: .infinity, minHeight: 65, maxHeight: 65, alignment: .topLeading) } } .padding([.top, .bottom], 150.0) .padding([.leading, .trailing], 50.0) } } @ViewBuilder private func renderPhoto(phase: AsyncImagePhase, item: Foo, index: Int) -> some View { switch phase { case .success(let image): thumbnailView(image: image, item: item, index: index) case .failure(let error): thumbnailView(item: item, index: index, isFailure: true) case .empty: thumbnailView(item: item, index: index, isFailure: true) @unknown default: EmptyView() } } private func thumbnailView(image: Image? = nil, item: Foo, index: Int, isFailure: Bool = false) -> some View { VStack { Rectangle() .foregroundColor(.clear) .frame(width: 72, height: 55) .background( VStack { if let image = image { image.resizable() .aspectRatio(contentMode: .fill) // .aspectRatio(contentMode: .fit) /// Setting aspect ratio to fit avoids the problem, but doesn't give rounded rectangle look. .frame(width: 72, height: 55) .disabled(false) .clipped() } else { /// show error image EmptyView() } } ) .cornerRadius(8) .padding([.top, .bottom], 10.0) .onTapGesture { print("%%%%% Tapped image title: \(item.title) and index is: \(index) %%%%%%") } } } }
Screenshots showing current behavior:
Rounded rectangle images with aspectRatio(.fill):
Tapping the red part of Image A opens Image B (because Image B is wider).
Images with aspectRatio(.fit):
Tapping Image A and Image B correctly opens the respective images, but the desired rounded rectangle look is lost.
How can I achieve the rounded rectangle appearance while ensuring that tapping anywhere on Image A only opens Image A?
Comments 0
•Answers 2
•Views 57
Answer by TychoAI Agent • 1 month ago
You’re most likely running into an issue with SwiftUI’s hit-testing. When you use aspectRatio(.fill), the image fills the view and—even though it’s clipped visually—the hit test area may extend to the full area of the underlying view. As a result, the tap gesture “leaks” into the adjacent view when the image overflows its bounds.
A common solution is to explicitly set the hit-test shape using the contentShape modifier. For example, if you want the tap area to be limited to a rounded rectangle matching your visual clipping, you can add a contentShape with the same rounded rectangle:
SWIFTprivate func thumbnailView(image: Image? = nil, item: Foo, index: Int, isFailure: Bool = false) -> some View { VStack { Rectangle() .foregroundColor(.clear) .frame(width: 72, height: 55) .background( VStack { if let image = image { image.resizable() .aspectRatio(contentMode: .fill) .frame(width: 72, height: 55) .clipped() } else { // show error image EmptyView() } } ) .cornerRadius(8) // Define the hot area to match the visible rounded rectangle .contentShape(RoundedRectangle(cornerRadius: 8)) .padding([.top, .bottom], 10.0) .onTapGesture { print("%%%%% Tapped image title: \(item.title) and index is: \(index) %%%%%%") } } }
.cornerRadius(8)
..contentShape(RoundedRectangle(cornerRadius: 8))
, you explicitly restrict the tap detection area to the same rounded rectangle.This approach lets you maintain the desired rounded rectangle appearance while ensuring that taps are handled on the right image view.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by GalacticCollector579 • 1 month ago
I've run into this behavior before. clipped()
may visually clip the view, but it still accepts clicks/taps.
To solve this, you can add .allowsHitTesting(false)
to the Image
.
Then, you'll need to add .contentShape(.rect)
(or a rounded rect if you want) to your Rectangle
, since otherwise, the clear
color it has means that it won't accept hits.
Here's the modified thumbnail view:
SWIFTprivate func thumbnailView(image: Image? = nil, item: Foo, index: Int, isFailure: Bool = false) -> some View { VStack { Rectangle() .foregroundColor(.clear) .frame(width: 72, height: 55) .background( VStack { if let image = image { image.resizable() .aspectRatio(contentMode: .fill) .frame(width: 72, height: 55) .clipped() .allowsHitTesting(false) // <-- Here } else { /// show error image EmptyView() } } ) .cornerRadius(8) .padding([.top, .bottom], 10.0) .contentShape(.rect) // <-- Here .onTapGesture { print("%%%%% Tapped image title: \(item.title) and index is: \(index) %%%%%%") } } }
No comments yet.
No comments yet.