SwiftUI LazyVGrid点击图片出现错误选中问题求助
SwiftUI LazyVGrid 点击图片选中错误问题
使用SwiftUI的LazyVGrid展示图片时,点击部分图片会出现选中错误(选中的不是点击的那张),但并非所有图片都有此问题,部分图片可正常选中。
问题代码
import SwiftUI class LabeledImage: Identifiable, ObservableObject, Equatable { let id: String let image: UIImage let isOriginal: Bool @Published var isSelected: Bool init(id: String, image: UIImage, isOriginal: Bool, isSelected: Bool = false) { self.id = id self.image = image self.isOriginal = isOriginal self.isSelected = isSelected } static func == (lhs: LabeledImage, rhs: LabeledImage) -> Bool { lhs.id == rhs.id } } struct LabeledImageView: View { @ObservedObject var item: LabeledImage var imageSize: CGFloat var toggleAction: () -> Void var body: some View { ZStack(alignment: .topTrailing) { Image(uiImage: item.image) .resizable() .scaledToFill() .frame(width: imageSize, height: imageSize) .clipped() .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(item.isSelected ? Color.blue : Color.clear, lineWidth: 4) ) .onTapGesture { toggleAction() } .opacity(item.isSelected ? 0.8 : 1.0) Text(item.id) .font(.caption) .padding(6) .background(Color.yellow.opacity(0.85)) .clipShape(Capsule()) .padding(6) if item.isOriginal { Text("ORIGINAL") .font(.caption) .padding(6) .background(Color.yellow.opacity(0.85)) .clipShape(Capsule()) .padding(6) } } } } struct GooglePictureFetcherRoute: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext @Environment(\.horizontalSizeClass) private var sizeClass @Bindable var route: MyRoute @State private var selectedImages: [LabeledImage] = [] @State private var foundSightsNameFromGoogleSearch = "" @State private var allImages: [LabeledImage] = [] @State private var didInitializeSelection = false @State private var gridSize: Double = 300 @State private var isLoadingImages = false // Dynamische Grid-Spalten basierend auf Gerätegröße private var columns: [GridItem] { let minSize: CGFloat let spacing: CGFloat = 10 switch sizeClass { case .compact: // iPhone Portrait - kleinere Bilder, 1-2 Spalten minSize = 100 case .regular: // iPad oder iPhone Landscape - größere Bilder, mehr Spalten minSize = 300 default: minSize = 200 } return [GridItem(.adaptive(minimum: minSize), spacing: spacing)] } private var imageSize: CGFloat { switch sizeClass { case .compact: return 100 // Kleinere Bilder auf iPhone case .regular: return 300 // Größere Bilder auf iPad default: return 200 } } var body: some View { NavigationStack { ZStack { ScrollView { LazyVGrid(columns: columns, spacing: 10) { ForEach(allImages, id: \.id) { item in LabeledImageView(item: item, imageSize: imageSize) { toggleSelection(of: item) } } } .padding() } .opacity(isLoadingImages ? 0.3 : 1.0) // Wartespinner if isLoadingImages { VStack(spacing: 20) { ProgressView() .scaleEffect(1.5) .progressViewStyle(CircularProgressViewStyle(tint: .blue)) Text("Loading Better Pictures...") .font(.headline) .foregroundColor(.primary) Text("Search in Google for better Pictures") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding(30) .background( RoundedRectangle(cornerRadius: 16) .fill(.regularMaterial) .shadow(radius: 10) ) } } .navigationTitle("Choose two Pictures") .toolbar { ToolbarLeftBackandClose(dismissAction: { dismiss() } ) ToolbarSave(save: { if selectedImages.count >= 1 { print(selectedImages[0].isOriginal) if selectedImages[0].isOriginal, let img = selectedImages[0].image.jpegData(compressionQuality: 1.0) { print("Org1") route.image1 = img } else { print("Alt1") route.image1 = CommonFunctions.resizeImageIfLarge(selectedImages[0].image) } } if selectedImages.count >= 2 { print(selectedImages[1].isOriginal) if selectedImages[1].isOriginal, let img = selectedImages[1].image.jpegData(compressionQuality: 1.0) { print("Org2") route.image2 = img } else { print("Alt2") route.image2 = CommonFunctions.resizeImageIfLarge(selectedImages[1].image) } } DBFunctions.save(context: modelContext) dismiss() }) } } .onAppear { if !didInitializeSelection { for img in originalImages { allImages.append(img) } fetchBetterPictures() didInitializeSelection = true } if sizeClass == .compact { gridSize = 100 } } } private func toggleSelection(of item: LabeledImage) { if item.isSelected { item.isSelected = false selectedImages.removeAll { $0.id == item.id } } else if selectedImages.count < 2 { item.isSelected = true selectedImages.append(item) } } // ********************************************************** // MARK: Prepare my Orginal Images private var originalImages: [LabeledImage] { var originals: [LabeledImage] = [] if let img1 = route.image1, let img1U = PlatformImage(data: img1) { originals.append(LabeledImage(id: route.id.uuidString+"1", image: img1U, isOriginal: true, isSelected: true)) } if let img2 = route.image2, let img2U = PlatformImage(data: img2) { originals.append(LabeledImage(id: route.id.uuidString+"2", image: img2U, isOriginal: true, isSelected: true)) } return originals } // ********************************************************** // MARK: Fetch Pictures with better quality from Google func fetchBetterPictures() { isLoadingImages = true DispatchQueue.main.async { MapCalculations.fetchImagesFromGoogle(for: nil, route: route) { foundSightsName, pictures in if let foundSightsName = foundSightsName { foundSightsNameFromGoogleSearch = foundSightsName } //var loadedImages: [LabeledImage] = [] // Füge neue Google-Bilder hinzu if let pictures = pictures { allImages += pictures.map { LabeledImage(id: $0.id.uuidString, image: $0.image ?? PlatformImage(), isOriginal: false, isSelected: false) } } //allImages = loadedImages isLoadingImages = false } } } }
问题原因及修复方案
核心问题
LazyVGrid会复用视图单元格,而LabeledImage是引用类型的ObservableObject,当视图复用时,@ObservedObject绑定的实例可能和当前显示的图片不匹配,加上直接修改引用类型属性的操作,导致点击响应和状态显示混乱。
修复步骤
将
LabeledImage改为值类型(struct)
引用类型的状态管理在LazyVGrid的复用机制下容易出问题,改为值类型后状态变化更可控:struct LabeledImage: Identifiable, Equatable { let id: String let image: UIImage let isOriginal: Bool var isSelected: Bool init(id: String, image: UIImage, isOriginal: Bool, isSelected: Bool = false) { self.id = id self.image = image self.isOriginal = isOriginal self.isSelected = isSelected } static func == (lhs: LabeledImage, rhs: LabeledImage) -> Bool { lhs.id == rhs.id } }修改
LabeledImageView的状态绑定方式
用@Binding替代@ObservedObject,让父视图统一管理状态:struct LabeledImageView: View { @Binding var item: LabeledImage var imageSize: CGFloat var toggleAction: () -> Void var body: some View { ZStack(alignment: .topTrailing) { Image(uiImage: item.image) .resizable() .scaledToFill() .frame(width: imageSize, height: imageSize) .clipped() .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(item.isSelected ? Color.blue : Color.clear, lineWidth: 4) ) .onTapGesture { toggleAction() } .opacity(item.isSelected ? 0.8 : 1.0) Text(item.id) .font(.caption) .padding(6) .background(Color.yellow.opacity(0.85)) .clipShape(Capsule()) .padding(6) if item.isOriginal { Text("ORIGINAL") .font(.caption) .padding(6) .background(Color.yellow.opacity(0.85)) .clipShape(Capsule()) .padding(6) } } } }更新父视图的
ForEach和状态管理逻辑
使用绑定传递数据,通过修改数组中的值类型实例来更新状态,避免手动维护双数据源:// LazyVGrid中的ForEach修改为: ForEach($allImages) { $item in LabeledImageView(item: $item, imageSize: imageSize) { toggleSelection(of: item.id) } } // 替换toggleSelection方法: private func toggleSelection(of itemId: String) { guard let index = allImages.firstIndex(where: { $0.id == itemId }) else { return } var updatedItem = allImages[index] if updatedItem.isSelected { updatedItem.isSelected = false allImages[index] = updatedItem } else if allImages.filter({ $0.isSelected }).count < 2 { updatedItem.isSelected = true allImages[index] = updatedItem } } // 移除原有的@State selectedImages,改为计算属性: private var selectedImages: [LabeledImage] { allImages.filter { $0.isSelected } }调整保存逻辑
保存时直接使用计算出来的selectedImages即可,无需修改原有保存代码,因为计算属性会实时返回选中的图片。
内容的提问来源于stack exchange,提问作者user30645598
相关产品推荐
相关产品推荐

