SwiftUI Combine数据流问题:训练组输入数据无法持久化保存
你的数据同步失效完全是属性包装器用法和值/引用类型混用导致的,和@Binding本身无关,核心错误有3个:
- 第一,
CompletedExercise、ExerciseSet被定义为class引用类型,同时给内部属性加了@Published,但嵌套的ObservableObject子类内部属性变更,只会触发自身的objectWillChange通知,不会向上传递给持有它的ExerciseStore,你在输入框修改值的时候,顶层的store完全感知不到变化,持久化时自然拿不到新数据。 - 第二,对引用类型数组用下标取绑定的写法
$exercise.sets[0]无效:class数组的下标访问拿到的是引用的临时拷贝,绑定不会指向原数组内存储的实例,你在SetView里修改的是临时拷贝的属性,原数组里的实例根本没有被更新。 - 第三,
@Published只能监听到值类型的属性变更、或者引用类型整体的指针替换,监听不到引用类型内部的属性变化,哪怕你把class实例存在@Published标注的数组里,修改实例内部属性也不会触发发布事件。
优先选第一种,完全符合SwiftUI的数据流设计规范,没有冗余代码:
方案1:将所有数据模型改为struct值类型(推荐)
值类型的变更会被@Published、@Binding自动感知,不需要做嵌套的通知传递,改完链路直接通:
- 把
CompletedExercise从class改为struct,移除ObservableObject协议遵循,删掉sets属性前的@Published - 把
ExerciseSet从class改为struct,移除ObservableObject协议遵循,删掉reps、weight属性前的@Published
修正后的模型代码如下,因为struct的所有属性都遵循Codable,你可以删掉手动写的编码解码逻辑,编译器会自动生成正确实现:
struct CompletedExercise: Codable, Identifiable { var id = UUID().uuidString var exercise: Exercise var sets = [ExerciseSet(), ExerciseSet(), ExerciseSet()] init(exercise: Exercise) { self.exercise = exercise } } struct ExerciseSet: Codable { var reps: Int? var weight: Int? init() {} } struct Exercise: Codable, Identifiable { var id = -1 var name = "Bench Press" }
改完之后绑定链路会自动生效:输入框修改值 → 绑定逐层更新struct属性 → 触发ExerciseStore的@Published发布事件,此时调用save()就能拿到最新数据。
方案2:保留class模型(不推荐)
如果业务逻辑必须用引用类型,需要手动给每一层嵌套的ObservableObject实例加订阅,把内部的变更事件转发给上层store,同时不能直接用数组下标取class实例的绑定,需要从store层根据id查找对应实例生成绑定。这个方案需要写大量样板代码,很容易出现循环引用、内存泄漏问题,非必要不要用。
你当前代码没有触发save()的时机,哪怕数据链路通了,修改完值不调用save也不会持久化到UserDefaults,可以给ExerciseStore的completedExercises加didSet监听器,只要数据变更就自动调用save,不需要手动触发:
class ExerciseStore: ObservableObject { @Published var completedExercises: [CompletedExercise] = [CompletedExercise(exercise: Exercise())] { didSet { save() } } // 其余原有代码保持不变 }
注意:TextField配合NumberFormatter处理空输入时有已知的绑定失效问题,建议给输入框加数字键盘限制,或者用@FocusState在输入结束时做类型转换,避免输入空值时绑定重置。
内容的提问来源于stack exchange,提问作者Pavel B.

