2
\$\begingroup\$

I'm building a simple iOS app using Swift that calculates the One Rep Max weight a user can theoretically lift. User inputs a weight lifted, the number of repetitions and the app uses one of several formulas to calculate the maximum weight they should be able to lift one time.

I'm skeptical about the way I'm implementing this whole thing because I'm using enum for the formula names but because I'm also providing the ability to save lift events in a log using Core Data, I have an entity called Formula with a relationship to a LiftEvent.

It works, but I think it smells and I would really appreciate some direction from someone who can tell me if it's a bad idea to be using both an enum and an entity for these formulas. If I'm headed down the wrong path, I want someone to set me straight.

Here's the LiftEvent:

import Foundation
import CoreData

extension LiftEvent {

    @NSManaged var date: NSDate
    @NSManaged var liftEventUid: NSNumber
    @NSManaged var maxAmount: NSNumber
    @NSManaged var repetitions: NSNumber
    @NSManaged var weightLifted: NSNumber
    @NSManaged var calculation: Formula
    @NSManaged var lift: Lift
}

and the Formula entity:

import Foundation
import CoreData

extension Formula {

    @NSManaged var formulaName: String
    @NSManaged var formulaUid: NSNumber
    @NSManaged var used: NSSet
}

Here's the CalculatorBrain.swift containing the enum for the formula names and the calculation function:

import Foundation

let formulas = ["Epley", "Brzychi", "Baechle", "Lander", "Lombardi", "Mayhew Et.Al.", "O'Conner Et.Al."]

enum CalculationFormula: Int {
  case Epley
  case Brzychi
  case Baechle
  case Lander
  case Lombardi
  case MayhewEtAl
  case OConnerEtAl
  case TotalFormulas

  var formulaName: String {
    switch self {
    case .Epley:
        return "Epley"
    case .Brzychi:
      return "Brzychi"
    case .Baechle:
      return "Baechle"
    case .Lander:
      return "Lander"
    case .Lombardi:
      return "Lombardi"
    case .MayhewEtAl:
      return "Mayhew Et.Al."
    case .OConnerEtAl:
      return "O'Conner Et.Al."
    default:
      return "Not Found"
    }
  }
}

class CalculatorBrain: NSObject {

  var weightLifted: Double?
  var repetitions: Double?
  var oneRepMax: Double?
  var preferredFormula: CalculationFormula

  override init() {
    let userDefaults = NSUserDefaults.standardUserDefaults()
    let tempDefault = userDefaults.objectForKey("preferredFormula") as! Int?
    self.preferredFormula = CalculationFormula.Epley
    if(tempDefault != nil) {
      self.preferredFormula = CalculationFormula(rawValue: tempDefault!)!
    }
}

  func calculateOneRepMax(weightLifted: Double, repetitions: Double, formula: CalculationFormula ) -> Double {
    switch formula {
    case .Epley:
      oneRepMax = weightLifted * (1 + (repetitions)/30)
      return oneRepMax!
    case .Brzychi:
      oneRepMax = weightLifted * (36/(37 - repetitions))
      return oneRepMax!
    case .Baechle:
      oneRepMax = weightLifted * ( 1 + ( 0.033 * repetitions ) )
      return oneRepMax!
    case .Lander:
      oneRepMax = 100 * weightLifted / (101.3 - (2.67123 * repetitions))
      return oneRepMax!
    case .Lombardi:
      //oneRepMax = weightLifted * Double((Int(repetitions) ^^ Int(0.10)))
      //return oneRepMax! //formula doesn't work
      return 1
    case .MayhewEtAl:
      oneRepMax = 100 * weightLifted / (101.3 - (2.67123 * repetitions))
      return oneRepMax!
    case .OConnerEtAl:
      oneRepMax = weightLifted * (1 + 0.025 * repetitions)
      return oneRepMax!
    default:
      return 1
    }
  }
}

Notice the raw value type of the enum is Int. I've chosen to do that because a) I need to use it to populate the table view below where the users can select one of them as the user default and I could only get the override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell to work as an Int and b) I also was only able to get the loop in the refresh() function to work if I made it an Int. This function is called when the user selects a formula and it places a checkmark on that selected formula. The selected formula is then set as the new default value and will be included in the lift log if the user chooses to save a lift in the lift log (this is done in another view).

Here's the table view controller where I use the enum as just described:

import UIKit

class FormulasTableViewController: UITableViewController {

  func refresh() {
    for index in 0 ... CalculationFormula.TotalFormulas.rawValue {
      let indexPath = NSIndexPath(forItem: index, inSection: 0)
      if let cell = tableView.cellForRowAtIndexPath(indexPath) {
        cell.accessoryType = calculator.preferredFormula.rawValue == index ? .Checkmark : .None
        print(cell.accessoryType.rawValue)
      }
    }
  }

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
    guard let theFormula = CalculationFormula(rawValue: indexPath.row) else {
      return
    }
    calculator.preferredFormula = theFormula
    let newPreferredFormula = calculator.preferredFormula
    savePreferences(newPreferredFormula)
    refresh()
  }

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    refresh()
  }

  override func viewDidLoad() {
    super.viewDidLoad()
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }

  // MARK: - Table view data source

  override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
  }

  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return formulas.count
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
    let currentFormula = formulas[indexPath.row]
    cell.textLabel?.text = currentFormula
    return cell
  }

  func savePreferences(formula: CalculationFormula) {
    let userDefaults = NSUserDefaults.standardUserDefaults()
    userDefaults.setObject(formula.formulaName, forKey: "preferredFormula")
  }

  var currentFormula: String {
    get {
      let userDefaults = NSUserDefaults.standardUserDefaults()
      if let returnValue = userDefaults.objectForKey("preferredFormula") as? String {
        return returnValue
      } else {
        return "Epley"
      }
    }
    set {
      NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: "preferredFormula")
      NSUserDefaults.standardUserDefaults().synchronize()
    }
  }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This is a perfect "question" for this site!

I see no problem with mapping an enum to an NSManagedObject. In fact I recommend it.

I think it is far better to encapsulate your core data classes, such that none of the rest of the code relies on the fact that core data is used. (Note that this was also expressed by Andy Matuschak in his "Refactor the Mega Controller" talk.)

Edit: I understand your concern about duplicating code, but this isn't code duplication... Think about it this way, the Federal Government refers to you by your social security number, while your state likely uses your driver's license number or some state ID number. Meanwhile, your friends refer to you by your name and your family uses your nick/pet name.

This isn't duplication, rather it is simply multiple ways to refer to the same thing so that the thing can be more easily referenced in different places.


The value type of the enum need not be an Int. You could just as easily make the value type a String and make a "formulaID" calculated property that returns the int (instead of "formulaName".)

EDIT: To expand on the above a bit... If you had the computed property below, I expect you could figure out how to use the enum values in the cellForRowAtIndexPath method and the refresh method, even if the enum used a String value type:

var formulaID: Int {
    switch self {
        case .Epley:
            return 0
        case .Brzychi
            return 1
        case .Baechle
            return 2
        case .Lander
            return 3
        case .Lombardi
            return 4
        case .MayhewEtAl
            return 5
        case .OConnerEtAl
            return 6
        default:
            fatalError()
    }
}

Based on your comments, the Int value type is being used to establish the position of the formula in a UI element and I think that's a very bad idea. If you later want to re-order the formulas, for e.g. to put the user's most favorite formula at the top of the list, you're in a bit of a pickle. Note, this isn't an argument for making the value type a string. Rather, it's an argument for dis-associating the value type from the position in the UITableView.


Another quick comment about your code... I strongly expect that TotalFormulas is not really a CalculationFormula type. It should be removed.

\$\endgroup\$
8
  • \$\begingroup\$ Daniel, I made the value type an Int because I couldn't figure out how to use the enum values in the cellForRowAtIndexPath method unless they were Ints. Similarly in the refresh() method, I didn't know how to make cell.accessoryType = calculator.preferredFormula.rawValue == index work without it being an Int. \$\endgroup\$
    – Jim
    Jun 20, 2016 at 5:03
  • \$\begingroup\$ Also, about the Core Data stuff... so having an enum with the formula names and also having those formula names in a database 'table' isn't considered duplicating code somehow? \$\endgroup\$
    – Jim
    Jun 20, 2016 at 5:07
  • 1
    \$\begingroup\$ I edited my answer to cover your additional concerns. \$\endgroup\$
    – Daniel T.
    Jun 20, 2016 at 13:07
  • \$\begingroup\$ Your suspicion about TotalFormulas is correct - it is not a real CalculationFormula type. That was a hacky way to set the upper bound in for loop of the refresh() method because I couldn't get a .count on the enum. \$\endgroup\$
    – Jim
    Jun 20, 2016 at 16:27
  • \$\begingroup\$ Instead, give the enum a count computed property that returns the count. \$\endgroup\$
    – Daniel T.
    Jun 20, 2016 at 16:32

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.