nc14
nc14

Reputation: 559

Not correctly referencing random number in Swift object

I have an array of objects where each object has an Exercise Name and a random number of reps.

I then have a function to generate a random workout (with between 3 - 6 exercises in it)

However, when I print it the reps are almost always 1, 2 or occasionally 14, despite loading it 30 times or so.

Am I doing something wrong here?

Here's my objects and struct :

struct exerciseInWorkout {

    let name : String
    let reps : Int

}

let exerciseBankArray = [

    exerciseInWorkout(name: "Squat", reps: (Int(arc4random_uniform(10)))),
    exerciseInWorkout(name: "Push Ups", reps: (Int(arc4random_uniform(5)))),
    exerciseInWorkout(name: "Viking Press", reps: (Int(arc4random_uniform(20)))),

]

and here's my function :

func generateWorkout(){

        let possibleExercises = exerciseBankArray
        let numberOfExercisesKey = Int(arc4random_uniform(4) + 3)

let workoutSet : [exerciseInWorkout] = (1...numberOfExercisesKey).map { _ in

let randomKey = Int(arc4random_uniform(UInt32(possibleExercises.count)))
            return exerciseInWorkout(name: exerciseBankArray[randomKey].name, reps: exerciseBankArray[randomKey].reps)}
        print (workoutSet)


        }

    }

Also, is there a way to create a set of these to avoid the same exercise coming up twice? I tried using Set but didn't seem to work at all.

Lastly, when I print it, each object is prepended with "project.exerciseInWorkout... is there any way to just print/return the clean array i.e. [["name: "press ups", reps: 12], [name:xyz, reps: 30]]?

Reason being I want to pass this to a new VC to put in a table view next and presume I need a clean array to do that.

Upvotes: 0

Views: 69

Answers (1)

Julien Perrenoud
Julien Perrenoud

Reputation: 1591

Short Answer

It looks like your array exerciseBankArray is stored globally, which means exerciseInWorkout components are initialized once for the whole app. Having said that, it is then normal that the number of reps is always the same, the arc4random_uniform is only executed on the first access.


Quick Fix

If you want to keep the same structure, I recommend this

  1. remove the arc4random_uniform from exerciseBankArray and just write the maximum amount of reps you want

    let exerciseBankArray = [
    
        exerciseInWorkout(name: "Squat", maxReps: 10),
        exerciseInWorkout(name: "Push Ups", maxReps: 5),
        exerciseInWorkout(name: "Viking Press", maxReps:  20)
    
    ]
    
  2. Call the random function inside generateWorkout() like this

    func generateWorkout(){
    
        let possibleExercises = exerciseBankArray
        let numberOfExercisesKey = Int(arc4random_uniform(4) + 3)
    
        let workoutSet : [exerciseInWorkout] = (1...numberOfExercisesKey).map { _ in
    
            let randomKey = Int(arc4random_uniform(UInt32(possibleExercises.count)))
            return exerciseInWorkout(
                name: exerciseBankArray[randomKey].name, 
                reps: Int(arc4random_uniform(exerciseBankArray[randomKey].maxReps))
             )
        } 
    }
    

Longer Improvements

If you're open to making a better architecture for your code, here are some suggestions

  1. Remove global variables
  2. Split your model for exercise into two classes / structs:

    • One that represents an actual exercise

      struct WorkoutExercise {
          let name: String
          let reps: Int
      }
      
    • One that represents a generator of exercise

      struct WorkoutExerciseGenerator {
          let name: String
          let maxReps: Int
          // Add any other parameter you need to generate a good exercise 
          func generate() -> WorkoutExercise {
              return WorkoutExercise(
                  name: name,
                  reps: Int(arc4random_uniform(maxReps))
              )
          }
      }
      

More Improvements / Q&A

When you say remove global variables do you mean store the array of exercises in each VC that needs them? I just thought that would be “repeating myself” (from DRY principles etc?)

I totally agree with the DRY guidelines, but there are many ways to not repeat yourself. The issue with global variables (a variable that is not inside any class, just free-floating) are numerous:

  1. It gets awkward when you want to include it in different targets
  2. It's not part of any namespace, so it might overload another one from another library of file, it messes up the auto-completion, etc...
  3. etc... you can find more documentation in this thread

Also, if I change to the 2nd example above, how would I then call the right amount of those? Just replace “return exerciseInWorkout” with the new function? Or would it be unchanged because the func is contained in the struct?

So I understand correctly, what you actually want to create is a set of default generators for exercises that have a name and a max count of reps, and these should be available in the whole project (hence why you used global variables).

A good way to improve this code is by defining static generators, for instance you can update WorkoutExerciseGenerator to be

        struct WorkoutExerciseGenerator {

            let name: String
            let maxReps: Int
            // Add any other parameter you need to generate a good exercise 

            func generate() -> WorkoutExercise {
                return WorkoutExercise(
                    name: name,
                    reps: Int(arc4random_uniform(maxReps))
                )
            }

            // Generates a "Squat" workout
            static var squat {
                 return WorkoutExerciseGenerator(name: "Squat", maxReps: 10)
            }

            // Generates a "Push Up" workout
            static var pushUp {
                 return WorkoutExerciseGenerator(name: "Push Ups", maxReps: 5)
            }

            // Generates a "Viking Press" workout
            static var vikingPress {
                 return WorkoutExerciseGenerator(name: "Viking Press", maxReps: 20)
            }
        }

Now that you have these specific generators, it looks like you also want to have a way to generate a whole workout. If that's the case, then you can simply create, in addition to what I wrote about, some objects to represent a workout and a workout generator.

/// This represents a whole workout that contains 
/// multiple exercises
struct Workout {

    let exercises: [WorkoutExercise]

}

/// This allows to dynamically creates a Workout
struct WorkoutGenerator {

    // This is the "pool" of exercises from
    // which it generates a workout (similar to your
    // `exerciseBankArray`)
    let exercisePool: [ExerciseGenerators]

    // Min and max amount of workouts
    let minCount: Int
    let maxCount: Int

    // Generates a workout from the generator
    func generate() -> WorkoutGenerator {

        let amount = Int(arc4random_uniform(maxCount - minCount)) + minCount
        let exercises = (0..<amount).map { _ in
           // Selects an exercise generator at random
           let index = Int(arc4random_uniform(exercisePool.count))
           // Generates a random workout exercise from this generator
           return exercisePool[index].generate()
        }

        return Workout(exercises: exercises)
    }

    // Same thing here, you can use a static variable to create
    // a "default" workout generator that contains the exercises
    // you had inside your `exerciseBankArray`
    static var default: WorkoutGenerator {
        return WorkoutGenerator(
            exercisePool: [.squat, .pushUp, .vikingPress],
            minCount: 3,
            maxCount: 6
        )
    }
}

Now that you have all of this, the only thing you need to do to create a totally random work-out according to your requirements is

let myWorkout = WorkoutGenerator.default.generate()

If you want to add more exercise types, just create more static ExerciseGenerator, and if you want to create different types of workouts (maybe with different exercise pools, some hard or some easy), just create additional static WorkoutGenerator. (Note that you don't need static, you can also just create the object directly in your VC).

Hope that helps!

Upvotes: 3

Related Questions