You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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绑定的实例可能和当前显示的图片不匹配,加上直接修改引用类型属性的操作,导致点击响应和状态显示混乱。

修复步骤

  1. 将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
        }
    }
    
  2. 修改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)
                }
            }
        }
    }
    
  3. 更新父视图的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 }
    }
    
  4. 调整保存逻辑
    保存时直接使用计算出来的selectedImages即可,无需修改原有保存代码,因为计算属性会实时返回选中的图片。

内容的提问来源于stack exchange,提问作者user30645598

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:12:31