SwiftUI中ForEach列表选择功能失效问题求助
SwiftUI中ForEach列表选择功能失效问题
在SwiftUI中使用List(selection: $selection)配合ForEach渲染Core Data的FetchedResults<Manufacturer>时,点击列表行无任何反应,但.onDelete功能正常。期望实现点击厂商行时,行内显示勾选标记,同时将selection变量设置为对应的Manufacturer对象。(注:此烟草相关应用仅用于学习,清楚无法上架App Store)
核心代码片段
@FetchRequest(fetchRequest: ContentView.manufacturerByName) var manufacturers:FetchedResults<Manufacturer> @State private var selection: Manufacturer? Section("Manufacturer") { List(selection: $selection) { ForEach(manufacturers, id:\.self) { manufacturer in Text(manufacturer.name ?? "--") } .onDelete { rows in for row in rows { let manufacturer = manufacturers[row] storageProvider.deleteManufacturer(manufacturer) } } } }
完整工程代码
MyApp.swift
@main struct MyApp: App { let storageProvider = StorageProvider.standard var body: some Scene { WindowGroup { ContentView(storageProvider: storageProvider) .environment(\.managedObjectContext, storageProvider.persistentContainer.viewContext) } } }
ContentView.swift
import SwiftUI import CodeScanner struct ContentView: View { @Environment(\.dismiss) var dismiss let storageProvider: StorageProvider // Tobacco @State var tobaccoName: String = "" @State var tobaccoNr: String? = nil @State var ean: String? = nil // EAN @State private var isPresentingScanner = false @State private var scannedCode: String? // Manufacturers @FetchRequest(fetchRequest: ContentView.manufacturerByName) var manufacturers: FetchedResults<Manufacturer> @StateObject var searchText = SearchText() @State private var selection: Manufacturer? var body: some View { Form { Section("Tobacco Details") { TextField("Name", text: $tobaccoName) TextField("Number (e.g. #017)", text: $tobaccoNr ?? "") .keyboardType(.decimalPad) HStack { TextField("ean", text: $ean ?? "") .keyboardType(.numberPad) Button { isPresentingScanner.toggle() } label: { Image(systemName: "camera") } } } Text(selection?.name ?? "no selection") Button { storageProvider.saveManufacturer(named: "Test") } label: { Text("create manufacturer") } Section("Manufacturer") { List(selection: $selection) { ForEach(manufacturers, id:\.self) { manufacturer in Label(manufacturer.name ?? "--", systemImage: selection == manufacturer ? "checkmark" : "circle") } .onDelete { rows in for row in rows { let manufacturer = manufacturers[row] } } } } } .navigationTitle("add Tobacco") .navigationBarTitleDisplayMode(.inline) // MARK: - EAN-Scan Sheet .sheet(isPresented: $isPresentingScanner) { CodeScannerView(codeTypes: [.ean13], showViewfinder: true) { response in switch response { case .success(let result): do { ean = result.string isPresentingScanner = false } case .failure(let error): print(error.localizedDescription) } } .presentationDetents([.medium]) } // MARK: - Bottom Button .safeAreaInset(edge: .bottom) { VStack { Button { if selection == nil { print("no manufacturer") } else { storageProvider.saveTabacco( named: tobaccoName, number: tobaccoNr, ean: ean, manufacturer: selection ) dismiss() } } label: { Text("save Tobacco") .font(.callout) .frame(maxWidth: .infinity, minHeight: 44) } .buttonStyle(.borderedProminent) .contentTransition(.identity) } .frame(maxWidth: .infinity) .padding([.horizontal, .top]) .background(.ultraThinMaterial) } // MARK: - Manufacturer Search-Filter .onReceive(searchText.$debounced) { query in guard !query.isEmpty else { manufacturers.nsPredicate = nil return } manufacturers.nsPredicate = NSPredicate(format: "%K CONTAINS[cd] %@", argumentArray: [#keyPath(Manufacturer.name), query]) } } } func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> { Binding( get: { lhs.wrappedValue ?? rhs }, set: { lhs.wrappedValue = $0 } ) }
StorageProvider.swift
public class StorageProvider { public static var standard = StorageProvider() public let persistentContainer: NSPersistentContainer public init() { persistentContainer = NSPersistentContainer(name: "DataModel") persistentContainer.loadPersistentStores(completionHandler: { description, error in if let error = error { fatalError("Core Data store failed to load with error: \(error)") } }) } } // MARK: - Functions to call from the Views public extension StorageProvider { func saveTabacco(named name: String, number: String?, ean: String?, manufacturer: Manufacturer?) { let tabacco = Tabacco(context: persistentContainer.viewContext) tabacco.name = name tabacco.number = number tabacco.ean = ean tabacco.manufacturer = manufacturer tabacco.creationDate = Date.now do { try persistentContainer.viewContext.save() print("Tabacco saved succesfully") } catch { persistentContainer.viewContext.rollback() print("Failed to save Tabacco: \(error)") } } func saveManufacturer(named name: String) { let manufacturer = Manufacturer(context: persistentContainer.viewContext) manufacturer.name = name do { try persistentContainer.viewContext.save() print("Manufacturer saved succesfully") } catch { persistentContainer.viewContext.rollback() print("Failed to save Manufacturer: \(error)") } } func getAllTabaccos() -> [Tabacco] { let fetchRequest: NSFetchRequest<Tabacco> = Tabacco.fetchRequest() do { return try persistentContainer.viewContext.fetch(fetchRequest) } catch { print("Failed to fetch tabacco: \(error)") return [] } } func deleteTabacco(_ tabacco: Tabacco) { persistentContainer.viewContext.delete(tabacco) do { try persistentContainer.viewContext.save() } catch { persistentContainer.viewContext.rollback() print("Failed to save context: \(error)") } } func deleteManufacturer(_ manufacturer: Manufacturer) { persistentContainer.viewContext.delete(manufacturer) do { try persistentContainer.viewContext.save() } catch { persistentContainer.viewContext.rollback() print("Failed to save context: \(error)") } } func updateTabacco(_ tabacco: Tabacco) { do { try persistentContainer.viewContext.save() } catch { persistentContainer.viewContext.rollback() print("Failed to save context: \(error)") } } } // MARK: - FetchRequest extension ContentView { static var tabaccosByName: NSFetchRequest<Tabacco> { let request: NSFetchRequest<Tabacco> = Tabacco.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \Tabacco.name, ascending: true)] return request } static var manufacturerByName: NSFetchRequest<Manufacturer> { let request: NSFetchRequest<Manufacturer> = Manufacturer.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \Manufacturer.name, ascending: true)] return request } }
临时解决方案
Section("Manufacturer") { List() { TextField("search for manufacturer", text: $searchText.text) if manufacturers.isEmpty { if searchText.debounced != "" { NavigationLink(destination: CreateManuView(storageProvider: storageProvider, manuName: searchText.text)) { Label("add **\(searchText.text)**", systemImage: "plus") .tint(.accentColor) } } else { Text("search for a manufacturer to add") .foregroundColor(.gray) } } ForEach(manufacturers) { manufacturer in Button(action: { selection = manufacturer }, label: { HStack { Text(manufacturer.name ?? "--") .tint(.primary) Spacer() if manufacturer == selection { Image(systemName: "checkmark") } } }) } .onDelete { rows in for row in rows { let manufacturer = manufacturers[row] storageProvider.deleteManufacturer(manufacturer) } } } }
问题原因分析
- List选择模式限制:SwiftUI的
List(selection:)默认适配多选场景(需配合EditButton触发编辑模式),在iOS的Form内嵌List中,原生选择交互会被Form的上下文逻辑覆盖,导致点击无响应。 - Core Data对象相等性问题:使用
id:\.self时,Core Data的NSManagedObject子类默认通过内存地址判断相等性,但FetchedResults更新时对象可能被重新实例化,导致selection == manufacturer判断失效。 - Form与List交互冲突:Form的单元格交互逻辑会拦截点击事件,阻止List的选择逻辑触发。
优化解决方法
方法1:启用原生List选择模式
保留List(selection:)的原生样式,需开启编辑模式并修正Core Data对象标识:
Section("Manufacturer") { List(selection: $selection) { ForEach(manufacturers, id: \.objectID) { manufacturer in Text(manufacturer.name ?? "--") } .onDelete { rows in for row in rows { let manufacturer = manufacturers[row] storageProvider.deleteManufacturer(manufacturer) } } } .listStyle(.plain) .toolbar { EditButton() } }
- 使用
id: \.objectID替代id:\.self,objectID是Core Data对象的唯一持久化标识,不受实例化变化影响。 - 添加
EditButton开启编辑模式,原生List选择需在编辑状态下触发。 - 设置
.listStyle(.plain)避免Form的默认样式干扰交互。
方法2:自定义单选交互(更灵活)
无需依赖原生选择模式,直接实现自定义点击逻辑:
Section("Manufacturer") { List { ForEach(manufacturers, id: \.objectID) { manufacturer in HStack { Text(manufacturer.name ?? "--") Spacer() if selection?.objectID == manufacturer.objectID { Image(systemName: "checkmark") .foregroundColor(.accentColor) } } .contentShape(Rectangle()) .onTapGesture { selection = manufacturer } } .onDelete { rows in for row in rows { let manufacturer = manufacturers[row] storageProvider.deleteManufacturer(manufacturer) } } } }
- 用
objectID进行相等性判断,避免Core Data对象实例变化导致的判断错误。 - 添加
.contentShape(Rectangle())确保整行区域可响应点击。 - 使用
.onTapGesture替代Button,避免破坏Form单元格的默认外观。
内容的提问来源于stack exchange,提问作者luegm.dev
相关产品推荐
相关产品推荐

