In my app, I need to show images in rounded rectangle shape on a horizontal scroll view and when I tap on the image, the image opens in full screen. I have multiple images, but for the same of keeping it simple, I have 2 images in my sample code below.
The second image (Image B) is very wide. To explain this question in an easy way, I have chosen first image (Image A) with 2 shades (yellow and red). If you tap on red color of Image A, the app behaves as if Image B was tapped and opens Image B instead of opening Image A. Tapping on yellow color of Image A, opens Image A correctly.
This is happening because Image B is wide and I am using image.resizeable().aspectRatio(**.fill**)
to display images in a rounded rectangle shape. If I use image.resizeable().aspectRatio(**.fit**)
, then tap behavior works fine i.e. if red color of Image A is tapped, then app opens Image A itself, however with aspectRatio(.fit), the images don't get displayed as rounded rectangle.
Executable sample code:
import 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) %%%%%%")
}
}
}
}
Sharing screenshots of the app using sample code:
Rounded rectangle images with aspectRatio(.fill), but tapping on red color of 1st image, opens 2nd image, because 2nd image is wide. This is the look of the images I want to have.
Images with aspectRatio(.fit), tapping on 1st image, opens 1st image and tapping on 2nd image, opens 2nd image. This is the tap behavior I want to have.
How can I have rounded rectangular looking images and also open correct image when tapped i.e. tapping anywhere on Image A should only open Image A?
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:
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)
.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) %%%%%%")
}
}
}