1
\$\begingroup\$

With this iOS app, the user can create workouts (e.g., Squat, Bench Press, etc) and then track their progress including current weight and number of weights. Performance calculations are also provided (e.g., how much total volume (weight * reps) has increased over time)

My code is at the stage where I have the basic mechanics down and I am now extending the app. Where I am seeking guidance is on the underlying foundation and layout of the primary classes such as Exercise() and WorkoutDiary(). I feel like I am duplicating code often, such as when my controller (ExerciseDetailViewController) calls Exercise.getLastWorkout() which then has to call WorkoutDiary.getLastWorkout(). This level of abstraction is manageable now, but I wonder how it will scale. Is this a proper design paradigm, or is there a better way?

Exercise is the primary model class which the controller makes requests from:

class Exercise: NSObject, NSCoding {
    var name: String
    var notes: String?
    private var workoutDiary = WorkoutDiary(diary: [])
    var currentWeights = Weights(heavy: 0)

init(name: String, notes: String?, workoutDiary: WorkoutDiary, weight: Int) {
    self.name = name
    self.notes = notes
    self.workoutDiary = workoutDiary
    self.currentWeights.heavy = weight
}

// MARK: Types

// weights of the exercise
struct Weights {
    var heavy = 0
    var warmup25: Int { return roundToFives(Double(heavy) * 0.25) }
    var warmup50: Int { return roundToFives(Double(heavy) * 0.50) }
}

struct PropertyKey {
    static let nameKey = "Exercise_name"
    static let notesKey = "Exercise_notes"
    static let workoutDiaryKey = "Exercise_workoutDiary"
    static let currentWeightsHeavyKey = "Exercise_currentWeightsHeavy"
}

func recordWorkout(date: String, weight: Int, repsFirstSet: Int, repsSecondSet: Int) {
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yy-MM-dd"

    let newSets = [Set(weight: weight, repCount: repsFirstSet),Set(weight: weight, repCount: repsSecondSet)]
    let newWorkoutLogEntry = Workout(date: dateFormatter.dateFromString(date)!, sets: newSets)
    workoutDiary.addWorkout(newWorkoutLogEntry)

    //update weights
    self.currentWeights.heavy = weight
}

func getLastWorkout() -> Workout? {
    if let workout = workoutDiary.getLastWorkout() {
        return workout
    } else {
        return nil
    }
}

func getOldestWorkoutFromRange(dateRange: Int? = nil) -> Workout? {
    return workoutDiary.getOldestWorkoutFromRange(15)
}

func getBarWeightsString(targetWeight: Int) -> String {
    if targetWeight < 54 {
        return "Bar"
    } else {
        return calculatePlates(roundToFives(Double(targetWeight)))
    }
}

func getTotalVolumeIncrease(dateRange: Int) -> Int {
    let perfAnalyzer = PerformanceAnalyzer()
    return perfAnalyzer.calcTotalVolumeIncrease(workoutDiary, dateRange: dateRange)
}

// MARK: NSCoder

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
    aCoder.encodeObject(notes, forKey: PropertyKey.notesKey)
    aCoder.encodeObject(workoutDiary, forKey: PropertyKey.workoutDiaryKey)
    aCoder.encodeInteger(currentWeights.heavy, forKey: PropertyKey.currentWeightsHeavyKey)
}

required convenience init?(coder aDecoder: NSCoder) {
    let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
    let notes = aDecoder.decodeObjectForKey(PropertyKey.notesKey) as! String?
    let workoutDiary = aDecoder.decodeObjectForKey(PropertyKey.workoutDiaryKey) as! WorkoutDiary
    let currentWeights = aDecoder.decodeIntegerForKey(PropertyKey.currentWeightsHeavyKey)

    // Must call designated initializer.
    self.init(name: name, notes: notes, workoutDiary: workoutDiary, weight: currentWeights)
}
}

My Controller:

class ExerciseDetailViewController: UIViewController  {

@IBOutlet weak var display: UILabel!

// This value is either passed by `ExerciseTableViewController` in `prepareForSegue(_:sender:)`
var exercise: Exercise?

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view, typically from a nib.
    displayExercise()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

func displayExercise() {
    if let exercise = exercise {
        navigationItem.title = exercise.name

        let currentWeights = exercise.currentWeights
        let warmup25Text = String(currentWeights.warmup25) + " " + exercise.getBarWeightsString(currentWeights.warmup25)
        let warmup50Text = String(currentWeights.warmup50) + " " + exercise.getBarWeightsString(currentWeights.warmup50)
        let heavyText = String(currentWeights.heavy) + " " + exercise.getBarWeightsString(currentWeights.heavy)

        if let notes = exercise.notes {
            display.text! = "Notes:\n"
            display.text! += "\(notes)\n"
            display.text! += "\n"
        }

        display.text! += "Warmup (25%): \(warmup25Text)\n"
        display.text! += "Warmup (50%): \(warmup50Text)\n"
        display.text! += "Heavy (100%): \(heavyText)\n"
        display.text! += "\n"

        if let lastWorkout = exercise.getOldestWorkoutFromRange(15) {
            display.text! += "\(NSDateToPrettyString(lastWorkout.date)) Reps @\(lastWorkout.sets[0].weight): \(lastWorkout.sets[0].repCount) and \(lastWorkout.sets[1].repCount)\n"
        }

        if let lastWorkout = exercise.getLastWorkout() {
            display.text! += "\(NSDateToPrettyString(lastWorkout.date)) Reps @\(lastWorkout.sets[0].weight): \(lastWorkout.sets[0].repCount) and \(lastWorkout.sets[1].repCount)\n"
            display.text! += "\n"
            display.text! += "15-day total volume increase: \(exercise.getTotalVolumeIncrease(15))%"
            display.text! += "\n"

        }

    }
}
}

Workout class, part of my model, a core data structure used by WorkoutDiary, which is stored in Exercise:

class Workout: NSObject, NSCoding {
var date: NSDate
var sets: [Set]

init(date: NSDate, sets: [Set]) {
    self.date = date
    self.sets = sets
}

struct PropertyKey {
    static let dateKey = "Workout_date"
    static let setsKey = "Workout_sets"
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(date, forKey: PropertyKey.dateKey)
    aCoder.encodeObject(sets, forKey: PropertyKey.setsKey)
}

required convenience init?(coder aDecoder: NSCoder) {
    let date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as! NSDate
    let sets = aDecoder.decodeObjectForKey(PropertyKey.setsKey) as! [Set]

    self.init(date: date, sets: sets)
}

}

Here is where the Workouts live:

class WorkoutDiary: NSObject, NSCoding {
var diary: [Workout] = []

init(diary: [Workout]) {
    self.diary = diary
}

struct PropertyKey {
    static let diaryKey = "WorkoutDiary_diary"
}

func addWorkout(workout: Workout) {
    diary.append(workout)
}

func getLastWorkout() -> Workout? {
    return diary.last
}

func getOldestWorkoutFromRange(dateRange: Int? = nil) -> Workout? {
    // if diary is empty, return nil
    if diary.count == 0 {
        return nil
    }

    // if an argument was passed
    if let dateRange = dateRange {
        let daysAgo = calcDaysAgo(dateRange)

        // iterate through Diary in reverse to find first workout that is older than dateRange
        for workout in diary.reverse() {

            // find the first workout that is older than our dateRange
            if workout.date < daysAgo {

                // if that workout happens to be our last workout, return nil
                if workout == diary.last {
                    return nil
                }

                // otherwise, return the workout just ahead of the matched workout
                return diary[diary.indexOf(workout)!+1]
            }
        }

        // if we don't find any workouts that are older than our range, return the first
        return diary.first
    } else {

        // if no argument, just return the first element
        return diary.first
    }
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(diary, forKey: PropertyKey.diaryKey)
}

required convenience init?(coder aDecoder: NSCoder) {
    let diary = aDecoder.decodeObjectForKey(PropertyKey.diaryKey) as! [Workout]

    self.init(diary: diary)
}

}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ HI. Welcome to Code Review! We title based on what function the code performs, not on how the code works. What task is the code supposed to accomplish? \$\endgroup\$ – mdfst13 Apr 30 '16 at 4:09
  • \$\begingroup\$ Got it. Thanks for the edits for clarity. This app is designed to track workouts and analyze performance \$\endgroup\$ – briancl Apr 30 '16 at 12:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.