SwiftUI删除字典分组Core Data列表项报错及索引异常问题
SwiftUI+Core Data 分组列表删除数据异常修复
问题背景
开发支出记账应用时,将FetchRequest获取的ExpenseLog按日期分组为Section列表展示,删除数据时遇到核心问题:
- 初始状态下列表删除索引不匹配,无法定位正确待删除项
- 调整删除逻辑适配分组结构后,触发类型错误:Cannot convert value of type '[ExpenseLog]' to expected argument type 'NSManagedObject'
- 移除外层生成分组的
ForEach后删除功能恢复正常,但需要保留按日期分区的展示形式。
问题根因
.onDelete挂载位置错误:将删除回调绑定在遍历分组数组的外层ForEach上,此时回调返回的IndexSet对应分组层级的索引,指向的是整个[ExpenseLog]分组,而非单条日志对象,直接触发类型错误- 索引映射逻辑错误:删除时先修改了本地
sections数组副本再取元素,拿到的是分组数组而非单条ExpenseLog实例 - 未考虑搜索过滤的影响:内层列表展示的是过滤后的数据,直接从原始
sections数组按索引取数,会和UI实际展示的内容错位。
原有问题代码
主视图实现
import SwiftUI import CoreData struct LogListView: View { @Environment(\.managedObjectContext) var moc @FetchRequest(sortDescriptors: [SortDescriptor(\ExpenseLog.date, order: .reverse)]) var logs: FetchedResults<ExpenseLog> @State var logToEdit: ExpenseLog? @State var searchText = "" var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.dateStyle = .long return formatter } var sections: [[ExpenseLog]] { ExpenseLog.update(Array(logs), and: dateFormatter) } var body: some View { List { ForEach(sections, id: \.self) { (section: [ExpenseLog]) in Section(header: Text(dateFormatter.string(from: section[0].date!))) { ForEach(section.filter({ self.searchText.isEmpty ? true : $0.name!.localizedStandardContains(self.searchText) }), id: \.id) { log in Button { logToEdit = log } label: { HStack(spacing: 16) { CategoryIconsView(category: log.categoryEnum) VStack(alignment: .leading, spacing: 8) { Text(log.nameText).font(.subheadline) Text(log.dataText) } Spacer() Text(log.typeTransaction != "expensive" ? log.amountText : "-\(log.amountText)").font(.subheadline).foregroundColor(log.typeTransaction == "income" ? .green : .primary) } .padding(.vertical, 4) } } } } .onDelete(perform: deleteItem) } } func deleteItem(at offsets: IndexSet) { var sec = sections sec.remove(atOffsets: offsets) offsets.forEach { index in let log = sec[index] moc.delete(log) // 类型报错位置 try? moc.saveContext() }
日期分组扩展方法
static func update(_ result: [ExpenseLog], and dateFormatter: DateFormatter) -> [[ExpenseLog]] { return Dictionary(grouping: result) { (element : ExpenseLog) in dateFormatter.string(from: element.date!) }.values.sorted() { $0[0].date! > $1[0].date! } }
修复方案
逻辑调整说明
- 将
.onDelete回调移到遍历单条日志的内层ForEach上,此时回调返回的索引对应当前分组内的行位置 - 删除时直接从当前UI展示的过滤后数组中取对应位置的日志对象,不需要手动维护分组数组副本,
@FetchRequest会在数据保存后自动刷新分组 - 不需要做复杂的跨分组索引映射,从根源避免索引错位问题。
修复后代码
- 调整
List内的视图实现:
var body: some View { List { ForEach(Array(sections.enumerated()), id: \.offset) { sectionIndex, section in Section(header: Text(dateFormatter.string(from: section[0].date!))) { // 提前生成当前分组过滤后的展示数组 let displayLogs = section.filter { searchText.isEmpty ? true : $0.name!.localizedStandardContains(searchText) } ForEach(displayLogs, id: \.id) { log in Button { logToEdit = log } label: { HStack(spacing: 16) { CategoryIconsView(category: log.categoryEnum) VStack(alignment: .leading, spacing: 8) { Text(log.nameText).font(.subheadline) Text(log.dataText) } Spacer() Text(log.typeTransaction != "expensive" ? log.amountText : "-\(log.amountText)").font(.subheadline).foregroundColor(log.typeTransaction == "income" ? .green : .primary) } .padding(.vertical, 4) } } // 在内层ForEach绑定删除回调,传入当前展示的数组 .onDelete { offsets in deleteItem(at: offsets, from: displayLogs) } } } } }
- 重写删除方法:
func deleteItem(at offsets: IndexSet, from displayLogs: [ExpenseLog]) { // 直接从当前展示数组取要删除的对象,不存在类型错误和索引错位 for offset in offsets { let targetLog = displayLogs[offset] moc.delete(targetLog) } // 保存上下文,建议加错误处理不要直接用try?吞错误 do { try moc.save() } catch { print("删除数据失败: \(error.localizedDescription)") } }
内容的提问来源于stack exchange,提问作者LucasC
相关产品推荐
相关产品推荐

