最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - LazyVGrid item hit test area is not expected - Stack Overflow

programmeradmin1浏览0评论

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
Add a comment  | 

1 Answer 1

Reset to default 1

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()) // 
发布评论

评论列表(0)

  1. 暂无评论