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

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()
               )
        }
    }
}

崩溃原因分析

  1. 绑定对象生命周期冲突:SetEditor通过@Bindable持有MySet实例,删除Exercise时SwiftData会级联删除关联的MySet(若模型配置了级联规则),但此时SetEditor仍持有已被删除实例的绑定引用,触发内存访问错误或状态不一致。
  2. 过滤数组索引不匹配:filteredExercises是基于@Query结果的过滤数组,删除逻辑使用该数组的索引,但子视图中MySet的实时状态更新可能导致主视图数组隐式刷新,索引偏移量失效,引发越界访问。
  3. 自定义绑定的潜在问题: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 18:34:52