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

Swift Data Routine模型深拷贝时worksets触发断点错误求助

Swift Data模型副本创建时的断点错误及模型优化方案

问题说明

在为Swift Data的Routine模型创建临时副本时,访问Routine.worksets的getter方法触发断点错误:Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1cc1698ec)。核心需求是:用户在训练视图中修改内容(增删Workout或WorkoutSet)后,可选择覆盖原Routine或保留原版本。

现有模型结构

Routine模型及扩展

@Model
class Routine {
    var name: String
    var target: [Target]
    var worksets: [Workset]
    // 错误触发在worksets的getter中
    // Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1cc1698ec)
    var latestDate: Date
    
    init(name: String, target: [Target], worksets: [Workset], latestDate: Date) {
        self.name = name
        self.target = target
        self.worksets = worksets
        self.latestDate = latestDate
    }
    
    @Model
    class Workset {
        @Relationship(deleteRule: .cascade, inverse: \Workout.workset)
        var workout: Workout
        var workoutSets: [WorkoutSet]
        init(workout: Workout, workoutSets: [WorkoutSet]) {
            self.workout = workout
            self.workoutSets = workoutSets
        }
        @Model
        class WorkoutSet {
            var weight: Double
            var reps: Int
            var isDone: Bool
            init(weight: Double, reps: Int, isDone: Bool) {
                self.weight = weight
                self.reps = reps
                self.isDone = isDone
            }
            static func isDoneChange ( lhs: WorkoutSet, rhs: WorkoutSet) -> Bool {
                lhs.isDone == rhs.isDone
            }
        }
    }
}

extension Routine {
    func addWorkset(workout: Workout, context: ModelContext) {
        let newWorkset = Workset(workout: workout, workoutSets: [])
        context.insert(newWorkset)
        self.worksets.append(newWorkset)
    }
    func deepCopy() -> Routine {
            let copiedWorksets = self.worksets.map { workset in
                let copiedWorkout = Workout(name: workset.workout.name, target: workset.workout.target, tool: workset.workout.tool, maxWeight: workset.workout.maxWeight, isBookmarked: workset.workout.isBookmarked)
                let copiedWorkoutSets = workset.workoutSets.map { workoutSet in
                    Workset.WorkoutSet(weight: workoutSet.weight, reps: workoutSet.reps, isDone: workoutSet.isDone)
                }
                return Workset(workout: copiedWorkout, workoutSets: copiedWorksets)
            }
        return Routine(name: self.name, target: self.target, worksets: copiedWorksets, latestDate: self.latestDate)
        }
}

Workout模型

@Model
class Workout: Identifiable,Equatable, Hashable{
    var workset: Routine.Workset?
    let id: UUID
    var name: String
    var target: [Target]
    var tool: WorkTool
    var maxWeight: Double
    var isBookmarked: Bool
    
    init(id: UUID = UUID(), name: String, target: [Target], tool: WorkTool, maxWeight: Double, isBookmarked: Bool) {
        self.id = id
        self.name = name
        self.target = target
        self.tool = tool
        self.maxWeight = maxWeight
        self.isBookmarked = isBookmarked
    }
    static func == (lhs: Workout, rhs: Workout) -> Bool {
        return lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

枚举定义

enum WorkTool: String, CaseIterable, Codable {
    case barbell
    case dumbbell
    case machine
    case bodyWeight
    
    var stringValue: String {
        switch self {
        case .barbell:
            return "Barbell"
        case .dumbbell:
            return "Dumbbell"
        case .machine:
            return "Machine"
        case .bodyWeight:
            return "Body Weight"
        }
    }
}
enum Target: String, CaseIterable, Codable {
    case chest
    case back
    case legs
    case shoulders
    case biceps
    case triceps
    case abs
    
    static func stringValues(targets: [Target]) -> String {
        return targets.map { $0.rawValue }.joined(separator: ", ")
    }
}

视图使用流程

  • Main视图通过@Query获取Routine列表,点击后将选中的Routine传递给RoutineDetailView
  • RoutineDetailView中调用deepCopy()方法创建临时Routine副本
  • WorkingOutView提供输入框与按钮,用于记录训练状态并更新Routine

模型优化建议

1. 移除嵌套的@Model定义

Swift Data不建议在@Model类内部嵌套另一个@Model类(如Routine.Workset、Workset.WorkoutSet),这种嵌套会导致模型上下文管理混乱,容易触发访问错误。建议将Workset和WorkoutSet改为独立的@Model类:

@Model
class Routine {
    var name: String
    var target: [Target]
    @Relationship(deleteRule: .cascade, inverse: \Workset.routine)
    var worksets: [Workset]
    var latestDate: Date
    
    init(name: String, target: [Target], worksets: [Workset] = [], latestDate: Date) {
        self.name = name
        self.target = target
        self.worksets = worksets
        self.latestDate = latestDate
    }
}

@Model
class Workset {
    @Relationship(inverse: \Routine.worksets)
    var routine: Routine?
    @Relationship(deleteRule: .cascade, inverse: \Workout.workset)
    var workout: Workout
    @Relationship(deleteRule: .cascade, inverse: \WorkoutSet.workset)
    var workoutSets: [WorkoutSet]
    
    init(workout: Workout, workoutSets: [WorkoutSet] = []) {
        self.workout = workout
        self.workoutSets = workoutSets
    }
}

@Model
class WorkoutSet {
    @Relationship(inverse: \Workset.workoutSets)
    var workset: Workset?
    var weight: Double
    var reps: Int
    var isDone: Bool
    
    init(weight: Double, reps: Int, isDone: Bool) {
        self.weight = weight
        self.reps = reps
        self.isDone = isDone
    }
    
    static func isDoneChange(lhs: WorkoutSet, rhs: WorkoutSet) -> Bool {
        lhs.isDone == rhs.isDone
    }
}

2. 正确声明双向关系

所有关联关系需明确双向绑定,确保Swift Data能正确追踪对象间的引用。比如Routine与Workset、Workset与WorkoutSet之间都要添加对应的@Relationship逆属性。

3. 调整副本创建方式

Swift Data的@Model对象是托管在ModelContext中的,直接通过构造函数创建副本时,新对象需要关联到临时上下文(而非原上下文),避免与原数据产生引用冲突。修改deepCopy方法,传入临时上下文:

extension Routine {
    func deepCopy(context: ModelContext) -> Routine {
        let copiedRoutine = Routine(name: self.name, target: self.target, latestDate: self.latestDate)
        context.insert(copiedRoutine)
        
        for workset in self.worksets {
            // 复制Workout,若需关联原Workout可跳过此拷贝步骤
            let copiedWorkout = Workout(name: workset.workout.name, target: workset.workout.target, tool: workset.workout.tool, maxWeight: workset.workout.maxWeight, isBookmarked: workset.workout.isBookmarked)
            context.insert(copiedWorkout)
            
            let copiedWorkset = Workset(workout: copiedWorkout)
            context.insert(copiedWorkset)
            copiedRoutine.worksets.append(copiedWorkset)
            
            for set in workset.workoutSets {
                let copiedSet = WorkoutSet(weight: set.weight, reps: set.reps, isDone: set.isDone)
                context.insert(copiedSet)
                copiedWorkset.workoutSets.append(copiedSet)
            }
        }
        return copiedRoutine
    }
}

4. 隔离副本与原数据的上下文

在WorkingOutView中使用独立的临时ModelContext管理副本,用户确认保存后,再将副本的数据同步到原上下文的原模型中,或直接将副本插入原上下文替换原数据。

内容的提问来源于stack exchange,提问作者Butee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 14:57:05