Swift中如何修复“Attempt to delete section containing first responder...”错误?
错误信息
Thread 1: "Attempt to delete section containing first responder that refused to resign.\nFirst responder that was asked to resign (returned YES from -resignFirstResponder): <UITextField: 0x130146000; frame = (0 0; 249 22); opaque = NO; autoresize = W+H; gestureRecognizers = <NSArray: 0x600001df3750>; placeholder = Enter definition; borderStyle = None; background = <_UITextFieldNoBackgroundProvider: 0x6000010e3620: textfield=<UITextField 0x130146000>>; layer = <CALayer: 0x60000122c240>> inside containing view: <SwiftUI.ListCollectionViewCell: 0x1341bec00; baseClass = UICollectionViewListCell; frame = (20 35; 353 361); clipsToBounds = YES; layer = <CALayer: 0x600001238320>> at index path: <NSIndexPath: 0x833f455114c3dfbc> {length = 2, path = 0 - 0}\nCurrent first responder: <TtGC7SwiftUI15CellHostingViewGVS_15ModifiedContentVS_14_ViewList_ViewVS_26CollectionViewCellModifier>: 0x13107e200; frame = (0 0; 353 361); autoresize = W+H; gestureRecognizers = <NSArray: 0x600001c333c0>; layer = <CALayer: 0x600001239ac0>> inside co"
问题场景
我用CoreData开发闪卡应用,添加数据阶段运行时出现上述崩溃。推测问题出在DataController的两个数组,或是点击“+”按钮时遍历dataController.termdefpairs的for循环。视图核心功能是添加闪卡数据创建卡组,点击“save”按钮会用CoreData保存上下文及所有数据。
相关代码
TermDefinitionView及关联视图
struct TermDefinitionView: View { @ObservedObject var dataController = DataController.shared @ObservedObject private var viewModel = TermDefinitionViewModel() @State var name = "" @State var term = "" @State var definition = "" @State var tag = ""// Separate state for the name @Environment(\.managedObjectContext) var managedObjectContext @Environment(\.dismiss) var dismiss @State var showSet = false //@Binding var saveSet: Bool @State var isShowingSet = false @State private var currentSet: SetEntity? //@Binding var redirectToSet: Bool var body: some View { ZStack { NavigationStack { VStack { Text("Name") .padding(.trailing, 220) .fontWeight(.bold) TextField("Enter name", text: $name) .frame(width: 250) .padding() .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray, lineWidth: 1)) List { // ForEach($dataController.termdefpairs) { _ in // TermView(term: term, definition: definition, tag: tag) // // } ForEach(dataController.termdefpairs.indices, id: \.self) { index in TermView(term: dataController.termdefpairs[index].term, definition: dataController.termdefpairs[index].definition, tag: dataController.termdefpairs[index].tag) } .onDelete { index in self.dataController.termdefpairs.remove(at: index.first!) } } .navigationBarItems(trailing: Button(action: { //for testForm in viewModel.termdefpairs {} dataController.termdefpairs.removeAll() dataController.save() //saving data }) { Text("Save") // }) Spacer() Button(action: { dataController.addNew() for pair in dataController.termdefpairs { dataController.add(term: pair.term, definition: pair.definition, tag: pair.tag, date: Date(), name: name) } name = "" isShowingSet = true dismiss() }) { Image(systemName: "plus") .resizable() .foregroundColor(.white) .font(.title2) .frame(width: 30, height: 30) } .frame(width: 40, height: 40) .background(Color.blue) .clipShape(Circle()) .padding() } } } } } struct TermDefinitionView_Previews: PreviewProvider { static var previews: some View { TermDefinitionView() } } struct TermView: View { @State var term: String @State var definition: String @State var tag: String @State private var isTagExpanded = false var body: some View { VStack(alignment: .leading, spacing: 20) { if isTagExpanded { VStack(alignment: .leading, spacing: 8) { Text("Tag") .font(.headline) TextField("Enter tag", text: $tag) .padding() .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray, lineWidth: 1)) } } Button(action: { isTagExpanded.toggle() }) { HStack { Spacer() Image(systemName: isTagExpanded ? "minus.circle.fill" : "plus.circle.fill") .resizable() .foregroundColor(.blue) .frame(width: 25, height: 25) Text(isTagExpanded ? "Hide Tag" : "Add Tag") .foregroundColor(.blue) .font(.headline) } } .padding(.bottom, -25) VStack(alignment: .leading, spacing: 8) { Text("Term") .font(.headline) TextField("Enter term", text: $term) .padding() .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray, lineWidth: 1)) } VStack(alignment: .leading, spacing: 8) { Text("Definition") .font(.headline) TextField("Enter definition", text: $definition) .padding() .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray, lineWidth: 1)) } } .padding() } } struct TermAndDefinition: Identifiable { var id = UUID() var term: String var definition: String var tag: String }
DataController代码
class DataController: ObservableObject { static let shared = DataController() @Published var termdefpairs: [TermAndDefinition] = [] @Published var savedFlash: [FlashCardData] = [] let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "CoreData") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } // super.init() container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump }) container.viewContext.automaticallyMergesChangesFromParent = true fetchRequest() } func addNew() { termdefpairs.append(TermAndDefinition(term: "", definition: "", tag: "")) // func add(term: String, definition: String, tag: String, date: Date, name: String) { // let data = FlashCardData(context: self.container.viewContext) // data.id = UUID() // data.definition = definition // data.term = term // data.tag = tag // data.date = date // data.name = name // fetchRequest() // } } func fetchRequest() { let request: NSFetchRequest<FlashCardData> = FlashCardData.fetchRequest() let sort = NSSortDescriptor(keyPath: \FlashCardData.date, ascending: false) request.sortDescriptors = [sort] do { self.savedFlash = try container.viewContext.fetch(request) } catch { print("Failed to fetch FlashCardData: \(error)") } } func save() { do { try container.viewContext.save() print("Data saved") } catch { print("We could not save the data...") } } func add(term: String, definition: String, tag: String, date: Date, name: String) { let data = FlashCardData(context: self.container.viewContext) data.id = UUID() data.definition = definition data.term = term data.tag = tag data.date = date data.name = name fetchRequest() }
CoreData结构

1. 崩溃原因
错误本质是:操作列表(清空/删除元素)时,列表内的UITextField仍处于编辑状态(第一响应者),SwiftUI尝试让其取消编辑状态时出现异常,触发崩溃。
2. 具体修复步骤
(1)修复TextField绑定同步问题
当前TermView的参数是值传递,修改输入框内容不会同步回dataController.termdefpairs,还会导致状态混乱,需改为绑定传递:
// 修改TermView参数为Binding类型 struct TermView: View { @Binding var term: String @Binding var definition: String @Binding var tag: String @State private var isTagExpanded = false // 其余视图代码保持不变 } // 在TermDefinitionView的List中使用绑定式ForEach List { ForEach($dataController.termdefpairs) { $pair in TermView(term: $pair.term, definition: $pair.definition, tag: $pair.tag) } .onDelete { indexSet in dataController.termdefpairs.remove(atOffsets: indexSet) } }
(2)操作数组前强制取消键盘编辑
在清空/删除数组前,手动让当前第一响应者取消编辑:
// 添加全局取消键盘的工具方法 extension View { func resignKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } // 修改Save按钮的action Button(action: { resignKeyboard() dataController.termdefpairs.removeAll() dataController.save() }) { Text("Save") } // 修改+按钮的action Button(action: { resignKeyboard() dataController.addNew() // 注意:原逻辑每次点击+都会重复保存所有已有数据,建议移到Save按钮统一保存 for pair in dataController.termdefpairs { dataController.add(term: pair.term, definition: pair.definition, tag: pair.tag, date: Date(), name: name) } name = "" isShowingSet = true dismiss() }) { // 按钮样式代码不变 }
(3)修复重复存储逻辑
原+按钮的遍历保存逻辑会导致重复存储已有数据,建议将批量保存逻辑移到Save按钮:
// Save按钮最终action Button(action: { resignKeyboard() // 批量保存所有闪卡数据 for pair in dataController.termdefpairs { dataController.add(term: pair.term, definition: pair.definition, tag: pair.tag, date: Date(), name: name) } dataController.save() dataController.termdefpairs.removeAll() name = "" isShowingSet = true dismiss() }) { Text("Save") } // +按钮仅负责添加空输入项 Button(action: { resignKeyboard() dataController.addNew() }) { // 按钮样式代码不变 }
3. 额外优化建议
- 避免用
ForEach(indices)遍历数组,这种方式在数组元素变动时易出现状态不同步,优先使用绑定式ForEach($array)。 - 确保
DataController单例使用正确,当前TermDefinitionView中直接引用DataController.shared的方式是正确的。
内容的提问来源于stack exchange,提问作者Kolea

