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
相关产品推荐
相关产品推荐

