swiftuienumsswiftui-navigationview

SwiftUI change list value with enum associated values and arrays on different view using Navigation link


I have been unable to solve the problem for several days. There is a data type that includes an array with an enum. I want to зфыы the data to another view for editing. But can't understand how to bind it. Is this even possible.

P.S. Use MVVM architecture.

Here is my code

Data models:

struct SportProgramModel: Codable, Identifiable { //Ьodel for training program
    var id = UUID().uuidString
    let title: String
    let description: String
    let author: String
    let shared: Bool
    let trainingDays: [TrainingDayListValues]
}

struct TrainingDayListValues: Codable, Identifiable{ //Model for workout days: contain workouts or rest. Select by enum 
    var id = UUID().uuidString
    let trainingDayType: trainingDaysTypes
}

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

struct TrainingDayModel: Codable, Identifiable {
    var id = UUID().uuidString
    let title: String
    let setOfExercises: [SetOfExercisesModel]

View #1:

View model

@MainActor
class SportProgramViewModel: ObservableObject{
    @Published var sportProgram: SportProgramModel? 
    @Published var trainingDaysList: [TrainingDayListValues] = []
    
    func getSportProgramDetails(sportProgramID: String) async {
        do{
            let sportProgram = try await SportManager.shared.getSportProgram(id: sportProgramID) //get data from firebase
            self.trainingDaysList = sportProgram.trainingDays
        } catch {
            print("Error when getting sport program")
        }
    }
}

View

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

    @State private var title = ""
    @State private var description = ""
    @State private var author = ""
    @State private var shared: Bool = false

    @State private var restDays = ""
    @State private var showRestDayView: Bool = false

    var sportPorgramID: String?
    
    var body: some View {
        VStack{
            VStack {
                TextField("Name your program", text: $title)
                Divider()
                TextField("Enter descripton", text: $description)
                Text("author id" + author)
                }
          
            
            List{
                ForEach(sportProgramViewModel.trainingDaysList){day in
                    switch day.trainingDayType {
                    case .workout(let workout):
                        NavigationLink {
                            TrainingDayView(trainingDay: workout) //Here pass data to another view with binding, but how??
                        } label: {
                                Text(workout.title)
                        }
                    case .rest(let rest):
                        Text("Rest for " + rest + " days")

                    }
                }
            }
            
            // Create new empty
            HStack {
                NavigationLink{
// TODO:                   TrainingDayView()
                } label: {
                    HStack {
                        Image(systemName: "plus.app.fill")
                        Text("Add day")
                    }
                }

            }
        }
        .onAppear{
            //if we have id it means its not a new sport program
            if let id = sportPorgramID{
                Task{
                    await sportProgramViewModel.getSportProgramDetails(sportProgramID: id)
    //update data
                    title = sportProgramViewModel.sportProgram?.title ?? ""
                    description = sportProgramViewModel.sportProgram?.description ?? ""
                    author = sportProgramViewModel.sportProgram?.author ?? ""
                    shared = sportProgramViewModel.sportProgram?.shared ?? false
                }
            }
        }
        .toolbar{
            Button {
                Task{
                    guard let user = UserManager.shared.firebaseAuth.currentUser.userId else { return }
//TODO:                    await sportProgramViewModel.saveSportProgram()
                    dismiss() 
                }  
            } label: {
                Image(systemName: "checkmark.circle.fill")
            } 
        }
        .navigationTitle("New program")
    }

}

#Preview {
    
    SportProgramView()
    
}

View #2

struct TrainingDayView: View {
    @Environment(\.dismiss) var dismiss
    
    let trainingDay: TrainingDayModel? //get data from previous view

    @State var title: String = ""
    
    var body: some View {
        NavigationStack{
            VStack{
                TextField("Name yor day", text: $title)
            }
            .padding()
            }
        }
    }


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

Tried make like in this thread, but have a problem with enum.

How to edit an item in a list using NavigationLink?


Solution

  • I would add an optional workout property to TrainingDayTypes so you can easily form a binding to workout.

    enum TrainingDaysTypes: Codable{
        case workout(TrainingDayModel)
        case rest(String)
        
        var workout: TrainingDayModel? {
            get {
                switch self {
                case .workout(let trainingDayModel):
                    trainingDayModel
                case .rest(let string):
                    nil
                }
            }
            set {
                if let newValue {
                    self = .workout(newValue)
                }
            }
        }
    }
    

    You should also change TrainingDayListValues.trainingDayType to a var, since TrainingDayView is going to modify its value.

    struct TrainingDayListValues: Codable, Identifiable{ //Model for workout days: contain workouts or rest. Select by enum
        var id = UUID().uuidString
        var trainingDayType: TrainingDaysTypes
    //  ^^^
    //  here!
    }
    

    Now you can make TrainingDayView take a @Binding,

    struct TrainingDayView: View {
        @Environment(\.dismiss) var dismiss
        
        @Binding var trainingDay: TrainingDayModel?
    

    In SportProgramView, you should pass a binding to the ForEach initialiser, so you also get a binding in the body of the ForEach.

    ForEach($sportProgramViewModel.trainingDaysList){ $day in
        switch day.trainingDayType {
        case .workout(let workout):
            NavigationLink {
                TrainingDayView(trainingDay: $day.trainingDayType.workout)
            } label: {
                Text(workout.title)
            }
        case .rest(let rest):
            Text("Rest for " + rest + " days")
            
        }
    }