I am learning SwiftUI and trying to impl a photo gallery app, using LazyVGrid. In the ForEach loop of my ContentView's body() method, I have a thumbnail view for each photo fetched from the library.
import SwiftUI
import Photos
import CoreImage
import ImageIO
import AVKit
import AVFoundation
struct Content: Identifiable {
let id = UUID()
let phAsset: PHAsset
let index: Int
}
struct ContentView: View {
@State private var contents: [Content] = []
@State private var selectedContent: Content? = nil
@State private var isLoading: Bool = true
var body: some View {
NavigationView {
let minItemWidth: CGFloat = 100
let spacing: CGFloat = 2
let screenWidth = UIScreen.main.bounds.width
let columns = Int((screenWidth + spacing) / (minItemWidth + spacing))
let totalSpacing = spacing * CGFloat(columns - 1)
let itemWidth = (screenWidth - totalSpacing) / CGFloat(columns)
ZStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 2) {
ForEach(contents) { content in
ThumbnailView(content: content.phAsset)
.frame(width: itemWidth, height: itemWidth)
.onTapGesture {
selectedContent = content
}
.padding(4)
.background(Color.blue.opacity(0.5))
.clipped()
}
}
.padding(2)
}
.navigationTitle("Photos Library")
.onAppear(perform: loadContents)
.sheet(item: $selectedContent) { content in
NavigationView {
DetailImageView(asset: content.phAsset)
.navigationBarItems(trailing: Button("Done") {
selectedContent = nil
})
}
}
if isLoading {
ProgressView("Loading...")
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.5, anchor: .center)
}
}
}
}
func loadContents() {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
fetchContents()
}
}
}
func fetchContents() {
let fetchOptions = PHFetchOptions()
DispatchQueue.global(qos: .background).async {
self.isLoading = true
let fetchResult = PHAsset.fetchAssets(with: fetchOptions)
var fetchedContents: [Content] = []
var index = 0
fetchResult.enumerateObjects { (phAsset, _, _) in
fetchedContents.append(Content(phAsset: phAsset, index: index))
index += 1
}
self.isLoading = false
DispatchQueue.main.async {
contents = fetchedContents
}
}
}
}
struct ThumbnailView: View {
let content: PHAsset
@State private var image: UIImage? = nil
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
.scaledToFill()
} else {
Color.gray
}
}
.onAppear(perform: loadImage)
}
func loadImage() {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = false
imageManager.requestImage(for: content, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: requestOptions) { (image, _) in
self.image = image
}
}
}
struct DetailImageView: View {
let asset: PHAsset
@State private var image: UIImage?
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.all)
} else {
ProgressView()
}
}
.onAppear(perform: loadFullImage)
}
func loadFullImage() {
let manager = PHImageManager.default()
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
manager.requestImage(
for: asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFit,
options: options
) { result, _ in
image = result
}
}
}
The problem is that when I click on certain area of an item, the opened item is not expected. I tried debugging this issue for a whole day and found if I comment out the ".clipped()" modifier on the ThumbnailView, I can see the item views of the LazyVGrid is overlapped, that is the reason why the view on top is opened other than the view I thought I clicked. This is pretty strange to me as I believe if I specify the size of a view by .frame() modifier, it should not be able to receive any touch event outside the area.
I am new to SwiftUI and iOS programming, any help is very appreciated.
When an image is scaled to fill, the overflow is still receptive to taps, even if it is clipped.
To fix, try applying .contentShape
immediately after the .frame
modifier. This needs to be before the .onTapGesture
modifier:
ThumbnailView(content: content.phAsset)
.frame(width: itemWidth, height: itemWidth)
.contentShape(Rectangle()) // 👈 here
.onTapGesture {
selectedContent = content
}
// + other modifiers