0

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?

2 Answers 2

0

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")
        
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Yeah!! It works!! Thanks a lot!! By the way for pass data to another view I use let trainingDay: Binding<TrainingDayModel?> instead let trainingDay: TrainingDayModel?
0

This is for non-optional TrainingDayModel and one issue fix.

You can split your views to add binding layer by layer. Everything is just a normal binding, except the enum with associated value where you have to provide the Binding with init(get:set:). And in your model change mutable values to var.

updated list view, inside the Stack

TrainingDaysListView(trainingDaysList: $sportProgramViewModel.trainingDaysList)

struct TrainingDaysListView: View {

    @Binding var trainingDaysList: [TrainingDayListValues]
    
    var body: some View {
        List {
            ForEach($trainingDaysList) { $day in
                TrainingDayTypeView(trainingDayType: $day.trainingDayType)
            }
        }
    }
}

struct TrainingDayTypeView: View {
    
    @Binding var trainingDayType: trainingDaysTypes
    
    var body: some View {
        switch trainingDayType {
        case .workout(let workout):
            NavigationLink {
                TrainingDayView(
                    trainingDay:
                        Binding(get: {
                            workout
                        }, set: { newValue in
                            trainingDayType = .workout(newValue)
                        })
                )
            } label: {
                Text(workout.title)
            }
        case .rest(let rest):
            Text("Rest for " + rest + " days")
        }
    }
}

struct TrainingDayView: View {
    
    @Binding var trainingDay: TrainingDayModel
    
    var body: some View {
        NavigationStack{
            VStack{
                TextField("Name yor day", text: $trainingDay.title)
            }
            .padding()
        }
    }
}

Issue is whenever you navigate to TrainingDayView, update any value and go back to SportProgramView, the trainingDaysList will reset because of onAppear action

If you want to make onAppear to run only once, you can add didLoad state

@State private var didLoad: Bool = false

.onAppear {
    if !didLoad {
       ...
       didLoad = true
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.