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.
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.
Share Improve this question edited Feb 1 at 14:29 Robin asked Feb 1 at 13:23 RobinRobin 10.4k6 gold badges37 silver badges59 bronze badges 4- Please show a minimal reproducible example - some code that can be directly copy and pasted into a new project and reproduce the issue. – Sweeper Commented Feb 1 at 13:28
-
1
Similar to this post from earlier today. Try applying
.contentShape(Rectangle())
immediately after the.frame
modifier. – Benzy Neez Commented Feb 1 at 14:25 - @BenzyNeez thanks a lot! It works! Could you put it in an answer so I can accept it. You really saved my world! – Robin Commented Feb 1 at 14:35
- @Robin Pleased to hear it worked. I would have suggested marking this post as a duplicate of the one I referenced, but the only answer there has not been accepted or upvoted yet, which means that post can't be selected as the reference. So answer added here as requested. – Benzy Neez Commented Feb 1 at 15:04
1 Answer
Reset to default 1When 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()) //
暂无评论