Multilevel Array Appending

I'm working on a workout app which has an array within an array and takes user input to populate each array.

In the first array, I can add an item to plans using the addPlan function. However, I cannot seem to figure out how to add items to the next level in the addExercise function.

final class ViewModel: ObservableObject {
    @Published var plans: [Plan] = []
    
    func addPlan(_ item: String) {
        let plan = Plan(name: item, exercise: [])
        plans.insert(plan, at: 0)
    }
    func addExercise() {
        
    }
}

Essentially, Plan holds an array of Exercise. Here is the model for Plan

struct Plan: Identifiable {
    let id = UUID()
    let name: String
    var exercise: [Exercise]
    
    static let samplePlans = Plan(name: "Chest", exercise: [Exercise(name: "Bench"), Exercise(name: "Incline Bench")])
}

These two functions should behave the same but just appending the items into different arrays.

The end result would be:

Plan 1: [Exercise1, Exercise2, Exercise3]

Plan 2: [Exercise4, Exercise5, Exercise6] etc.

Project files: LINK

Accepted Reply

What JaiRajput said, plus.

When you add an exercise, you give it a name.

So code should be:

final class ViewModel: ObservableObject {
    @Published var plans: [Plan] = []
    
    func addPlan(_ item: String) {
        let plan = Plan(name: item, exercise: [])
        plans.insert(plan, at: 0)
    }

    func addExercise(to planIndex: Int, exoName: String) {
        if exoName == ""  { return }
        if planIndex < 0 || planIndex >= plans.count { return }

        // Also test exoName does not exist there already
        for exo in plans[planIndex].exercises {
            if exo.name == exoName { return }
        }
        // All ok, add to exercises. append will add as last exercise, insert(at: 0) would append as first.
        plans[planIndex].exercises.append(Exercise(name: exoName))
    }
}
  • The function looks good but since I'm using a UUID for the plan id instead of an Int, I am unable to give it a value when I call it in my view. Is it possible to have planIndex take a UUID instead of an Int? I tried switching the plan id to and Int but would just cause different errors.

Add a Comment

Replies

You add an exercise to a plan.

So, in addExercise, you should add a plan reference:

func addExercise(forPlan: Plan)

and then

forPlan.insert() // What you need to insert
  • Got the error "Value of type 'Plan' has no member 'insert'".

    It looks like that will add an element to Plan but I want to add an element to exercise which is an array in Plan

Add a Comment

I was not precise enough. Correction is straightforward.

forPlan.exercise.insert() // What you need to insert

Note: you'd better name it exercises and not exercise.

  • I did try that initially but got an error: "Cannot use mutating member on immutable value: 'forPlan' is a 'let' constant".

Add a Comment
    @Published var plans: [Plan] = []
    
    func addPlan(_ item: String) {
        let plan = Plan(name: item, exercises: [])
        plans.insert(plan, at: 0)
    }
    
    func addExercise(to planIndex: Int, exercise: Exercise) {
        guard planIndex >= 0 && planIndex < plans.count else {
            return 
        }
        
        plans[planIndex].exercises.append(exercise) 
    }
}

check this...

What JaiRajput said, plus.

When you add an exercise, you give it a name.

So code should be:

final class ViewModel: ObservableObject {
    @Published var plans: [Plan] = []
    
    func addPlan(_ item: String) {
        let plan = Plan(name: item, exercise: [])
        plans.insert(plan, at: 0)
    }

    func addExercise(to planIndex: Int, exoName: String) {
        if exoName == ""  { return }
        if planIndex < 0 || planIndex >= plans.count { return }

        // Also test exoName does not exist there already
        for exo in plans[planIndex].exercises {
            if exo.name == exoName { return }
        }
        // All ok, add to exercises. append will add as last exercise, insert(at: 0) would append as first.
        plans[planIndex].exercises.append(Exercise(name: exoName))
    }
}
  • The function looks good but since I'm using a UUID for the plan id instead of an Int, I am unable to give it a value when I call it in my view. Is it possible to have planIndex take a UUID instead of an Int? I tried switching the plan id to and Int but would just cause different errors.

Add a Comment

That will be a bit difficult to use id, as you do not know directly what it is.

You have 2 options:

  • create a planIndex
  • identify plan by its name, as it must be unique.

Second solution is the best.

Code will become:

  • to make sure name is unique:
    func addPlan(_ item: String) {
        if item == "" { return }
        // Also test item does not exist there already
        for plan in plans {
            if item == plan.name { return }
        }
        // now we can add safely
        let plan = Plan(name: item, exercises: [])
        plans.insert(plan, at: 0)
    }
  • use name to identify plan
    func addExercise(to planName: String, exoName: String) {
        if exoName == ""  { return }

        // find the index, which is unique
        guard let planIndex = plans.firstIndex(of: planName) else { return }

        // Also test exoName does not already exist in this plan
        for exo in plans[planIndex].exercises {
            if exo.name == exoName { return }
        }
        // All ok, add to exercises. append will add as last exercise, insert(at: 0) would append as first.
        plans[planIndex].exercises.append(Exercise(name: exoName))
    }
}

This should now work properly. If not, explain. If OK, don't forget to close the thread.

  • I got two errors. Both on the guard let line.

    First error was "Cannot convert value of type 'String' to expected argument type 'Plan'". I fixed this by changing the type of plan name from String to Plan.

    The second error was "Referencing instance method 'firstIndex(of:)' on 'Collection' requires that 'Plan' conform to 'Equatable'". I added Equatable to the Plan Model but wasn't sure what protocol stubs should be.

Add a Comment

Exact.

We must work on Strings:

let plansNames = plans.map { $0.name }
guard let planIndex = plansNames.firstIndex(of: planName) else { return }
  • That worked! Thanks for the help. Was stuck on this for a while!

Add a Comment