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

SwiftUI MVVM:如何绑定带关联值枚举的数组至编辑视图

解决方案:带关联值枚举的数组数据绑定完全可行

你的问题核心在于模型属性不可变以及绑定传递方式错误,以下是具体修正步骤:

1. 修正数据模型的可变性

所有需要编辑的属性必须改为var(原代码中大量使用let导致无法修改):

struct SportProgramModel: Codable, Identifiable {
    var id = UUID().uuidString
    var title: String // 改为var,支持编辑
    var description: String // 改为var
    let author: String
    var shared: Bool // 改为var
    var trainingDays: [TrainingDayListValues] // 改为var
}

struct TrainingDayListValues: Codable, Identifiable {
    var id = UUID().uuidString
    var trainingDayType: trainingDaysTypes // 改为var,支持修改枚举值
}

enum trainingDaysTypes: Codable { 
    case workout(TrainingDayModel)
    case rest(String)
}

struct TrainingDayModel: Codable, Identifiable {
    var id = UUID().uuidString
    var title: String // 改为var,支持编辑标题
    var setOfExercises: [SetOfExercisesModel] // 改为var,支持修改训练内容
}

// 补充缺失的SetOfExercisesModel定义(假设需要支持编辑)
struct SetOfExercisesModel: Codable, Identifiable {
    var id = UUID().uuidString
    var name: String
    // 其他训练属性...
}

2. 优化ViewModel结构

移除单独的trainingDaysList,直接操作sportProgram中的数组,确保数据同步:

@MainActor
class SportProgramViewModel: ObservableObject{
    @Published var sportProgram: SportProgramModel? 
    
    func getSportProgramDetails(sportProgramID: String) async {
        do{
            let sportProgram = try await SportManager.shared.getSportProgram(id: sportProgramID)
            self.sportProgram = sportProgram
        } catch {
            print("Error when getting sport program")
        }
    }
    
    // 新增:添加训练日的方法
    func addNewTrainingDay() {
        let newDay = TrainingDayListValues(trainingDayType: .workout(TrainingDayModel(title: "", setOfExercises: [])))
        sportProgram?.trainingDays.append(newDay)
    }
}

3. 父视图中传递枚举关联值的绑定

通过ForEach的索引绑定,找到对应的TrainingDayListValues元素,再针对.workout关联值创建绑定传递给子视图:

struct SportProgramView: View {
    @ObservedObject var sportProgramViewModel = SportProgramViewModel()
    @Environment(\.dismiss) var dismiss

    // 直接绑定ViewModel中的sportProgram属性,替代单独的State变量
    private var sportProgramBinding: Binding<SportProgramModel> {
        $sportProgramViewModel.sportProgram ?? .constant(SportProgramModel(title: "", description: "", author: "", shared: false, trainingDays: []))
    }
    
    var sportPorgramID: String?
    
    var body: some View {
        VStack{
            VStack {
                TextField("Name your program", text: sportProgramBinding.title)
                Divider()
                TextField("Enter description", text: sportProgramBinding.description)
                Text("author id: " + sportProgramViewModel.sportProgram?.author ?? "")
            }
          
            List{
                // 使用索引绑定来获取可修改的元素
                ForEach(sportProgramBinding.trainingDays.indices, id: \.self) { index in
                    let dayBinding = $sportProgramViewModel.sportProgram!.trainingDays[index]
                    switch dayBinding.wrappedValue.trainingDayType {
                    case .workout(let workout):
                        NavigationLink {
                            // 创建TrainingDayModel的绑定
                            let workoutBinding = Binding<TrainingDayModel>(
                                get: {
                                    if case .workout(let model) = dayBinding.wrappedValue.trainingDayType {
                                        return model
                                    } else {
                                        return TrainingDayModel(title: "", setOfExercises: [])
                                    }
                                },
                                set: { newModel in
                                    dayBinding.wrappedValue.trainingDayType = .workout(newModel)
                                }
                            )
                            TrainingDayView(trainingDay: workoutBinding)
                        } label: {
                            Text(workout.title)
                        }
                    case .rest(let rest):
                        Text("Rest for " + rest + " days")
                    }
                }
                .onDelete(perform: deleteDay) // 可选:添加删除功能
            }
            
            HStack {
                Button {
                    sportProgramViewModel.addNewTrainingDay()
                } label: {
                    HStack {
                        Image(systemName: "plus.app.fill")
                        Text("Add day")
                    }
                }
            }
        }
        .onAppear{
            if let id = sportPorgramID{
                Task{
                    await sportProgramViewModel.getSportProgramDetails(sportProgramID: id)
                }
            } else {
                // 如果是新建项目,初始化空模型
                sportProgramViewModel.sportProgram = SportProgramModel(title: "", description: "", author: "", shared: false, trainingDays: [])
            }
        }
        .toolbar{
            Button {
                Task{
                    guard let user = UserManager.shared.firebaseAuth.currentUser.userId else { return }
                    if let program = sportProgramViewModel.sportProgram {
                        // TODO: 保存逻辑,直接传入program即可
                        // await SportManager.shared.saveSportProgram(program)
                    }
                    dismiss() 
                }  
            } label: {
                Image(systemName: "checkmark.circle.fill")
            } 
        }
        .navigationTitle("New program")
    }
    
    private func deleteDay(at offsets: IndexSet) {
        sportProgramBinding.trainingDays.remove(atOffsets: offsets)
    }
}

#Preview {
    SportProgramView()
}

4. 修改子视图接收绑定

将子视图的let trainingDay改为Binding<TrainingDayModel>,实现双向绑定:

struct TrainingDayView: View {
    @Environment(\.dismiss) var dismiss
    @Binding var trainingDay: TrainingDayModel // 改为Binding
    
    var body: some View {
        NavigationStack{
            VStack{
                TextField("Name your day", text: $trainingDay.title)
                // 这里可以继续添加训练内容的编辑UI,直接绑定trainingDay.setOfExercises
            }
            .padding()
            .navigationTitle("Edit Training Day")
            .toolbar{
                Button("Done") {
                    dismiss()
                }
            }
        }
    }
}

#Preview {
    TrainingDayView(trainingDay: .constant(TrainingDayModel(title: "Leg Day", setOfExercises: [])))
}

关键说明

  • 枚举的关联值绑定需要手动创建Binding,通过get和set方法来读写枚举中的关联值
  • 所有需要编辑的模型属性必须是可变的(var),否则无法实现双向绑定
  • ViewModel应统一管理数据源,避免拆分数据导致同步问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:14:53