SwiftUI提取子视图后ForEach的onDelete功能崩溃问题排查
SwiftData删除功能崩溃问题(引入SetEditor后)
我有一个SwiftData模型Exercise,它包含一个MySet数组,每个MySet都以Exercise作为父模型。以下是相关代码及问题详情:
主视图查询与过滤代码
@Query(sort: [SortDescriptor(\Exercise.date, order: .reverse)]) private var exercises : [Exercise] private var filteredExercises: [Exercise] { exercises.filter { $0.workout == workout } }
主视图List与删除逻辑
Section(header: Text("Exercises")) { ForEach(filteredExercises){ exercise in VStack { Text("\(exercise.exerciseName.name)") SetsView(exercise: exercise) Button { if let exercisesets = exercise.sets { if(exercisesets.count <= 7) { let newSet = MySet(id: UUID(), weight: 0, reps: 0,isCompleted: false, exercise: exercise) // 添加后需刷新应用才可见 modelContext.insert(newSet) } } } label: { Text("Add set") .underline() } } } .onDelete(perform: delete) } // 删除方法 func delete(at offsets: IndexSet) { let exercisesToDelete = filteredExercises for index in offsets { let exercise = exercisesToDelete[index] modelContext.delete(exercise) } }
SetsView原代码(正常运行)
@Query(sort: [SortDescriptor(\MySet.weight)]) private var sets : [MySet] private var filteredSets: [MySet] { sets.filter { $0.exercise == exercise } } var body: some View { ForEach(filteredSets) { myset in HStack { Text("\(myset.weight)") Text("\(myset.reps)") } } }
引入SetEditor后代码(导致删除崩溃)
将SetsView中的遍历代码替换为以下内容后,onDelete功能失效并导致应用崩溃:
ForEach(filteredSets) { myset in HStack { SetEditor(myset: myset) } } // SetEditor子视图 struct SetEditor: View { @Bindable var myset: MySet var body: some View { VStack { Text("Weight") TextField( "\(myset.weight)", value: myset.weightBinding, formatter: NumberFormatter() ) } VStack { Text("Reps") TextField( "\(myset.reps)", value: myset.repBinding, formatter: NumberFormatter() ) } } }
崩溃原因分析
- 绑定对象生命周期冲突:
SetEditor通过@Bindable持有MySet实例,删除Exercise时SwiftData会级联删除关联的MySet(若模型配置了级联规则),但此时SetEditor仍持有已被删除实例的绑定引用,触发内存访问错误或状态不一致。 - 过滤数组索引不匹配:
filteredExercises是基于@Query结果的过滤数组,删除逻辑使用该数组的索引,但子视图中MySet的实时状态更新可能导致主视图数组隐式刷新,索引偏移量失效,引发越界访问。 - 自定义绑定的潜在问题:
weightBinding和repBinding若为自定义绑定属性,未正确处理模型被删除后的空值状态,也可能导致崩溃。
解决办法
1. 配置正确的级联删除规则
在Exercise模型中明确级联删除,确保删除父模型时自动清理关联子模型:
@Model class Exercise { var date: Date var workout: Workout? // 配置级联删除规则 @Relationship(deleteRule: .cascade) var sets: [MySet]? // 初始化方法及其他属性 }
2. 直接使用父模型的关联集合,避免全局Query过滤
修改SetsView,直接使用传入Exercise的关联sets,而非全局@Query后过滤,保证视图与父模型生命周期同步:
struct SetsView: View { let exercise: Exercise var body: some View { if let sets = exercise.sets { ForEach(sets) { myset in HStack { SetEditor(myset: myset) } } } } }
3. 修正删除逻辑,基于原始Query数组操作
filteredExercises的索引与原始exercises数组不匹配,删除时需先找到原始数组中的对应元素:
func delete(at offsets: IndexSet) { let filtered = filteredExercises offsets.forEach { index in let targetExercise = filtered[index] if let originalIndex = exercises.firstIndex(of: targetExercise) { modelContext.delete(exercises[originalIndex]) } } }
4. 优化SetEditor绑定逻辑
避免自定义绑定,直接使用@Bindable模型的原生属性:
struct SetEditor: View { @Bindable var myset: MySet var body: some View { HStack { VStack { Text("Weight") TextField("0", value: $myset.weight, formatter: NumberFormatter()) } VStack { Text("Reps") TextField("0", value: $myset.reps, formatter: NumberFormatter()) } } } }
内容的提问来源于stack exchange,提问作者john smith
相关产品推荐
相关产品推荐

