Remember to Remember

If the user finds a particular calculation's results valuable, it would be nice if the app possessed the ability to save them, or more specifically, save the particular set of inputs from which those results originated. This could be as simple as a screenshot of the Results Menu, either taken by the user, or as a function within the app. (Although the former seemed negligent and the latter somewhat crude).

Desired Behavior: Once the information is saved, it should be accessible, through another menu or screen, so that it can be both recalled (displayed in its original context), and subsequently available for adjustment, via the Inputs Menu, and applied to a new calculation.


Currently...

The entirety of the properties which constitute the inputs and results are static var's contained within the struct's Inputs.swift and Results.swift, respectively. Therefore, they and their constituent values may be accessed throughout the app, by any class or method which might need them.

The design's plan called for implementing a button in the Results Menu, which when tapped, would save the results and alert the user.

My first inclination was simply to collect the current results (since whatever figures currently displayed would correspond to the current values in Results) and package them in a new object which, in turn, could be saved.

However, the presented results, constrained by the views wherein they appear, are no longer numbers, but String's, the return type of the formatted[1] method of Results.

Even if this was not the case, it soon became clear that the important step, the one to which we were in fact attempting to return, was just prior to calculation. So, onto Inputs.


Inputs

Inputs.swift
struct Inputs {
  static var yearsToInvest: Float = Config.yearsToInvestDefault
  static var initialInvestment: Float = Config.initialInvestmentDefault
  static var monthlyDeposit: Float = Config.monthlyDepositDefault
  static var interestRate: Float = Config.interestRateDefault
  static var additionalFeeRate: Float = Config.additionalFeeRateDefault
  static var compoundInterval: Float = Config.compoundIntervalDefault
}

Now, these properties are ostensibly used for financial calculation, but here they are typed as Float's. This is because they interact, almost exclusively, with subclasses of UIControl, but they are converted before any calculation takes place.

When the results are on screen, these inputs can be collected and used to initialize a new object which, by conforming to and implementing the methods of NSCoding, would be capable of serialization.


Saved Results

The SavedResults class' properties mirror those of Inputs, but with the addition of accruedBalance (returned from Results' formatted method) and dateOfCalculation, an NSDate to be created when the class is initialized.

SavedResults.swift
class SavedResults: NSObject, NSCoding {
  var yearsToInvest: Float
  var initialInvestment: Float
  var monthlyDeposit: Float
  var interestRate: Float
  var additionalFeeRate: Float
  var compoundInterval: Float
  var accruedBalance: String
  var dateOfCalculation: NSDate

For the sake of simplicity, the inputs properties are typed as Float's here as well.

As a new SavedResults instance initializes its properties directly from Inputs and Results, one can be created at any time and be certain to contain the current values.

override init() {
  yearsToInvest = Inputs.yearsToInvest
  // The rest of the inputs...
  accruedBalance = Results.formatted(.AccruedBalance)
  dateOfCalculation = NSDate()
}

required init(coder decoder: NSCoder) {
  yearsToInvest = decoder.decodeObjectForKey("yearsToInvest") as! Float
  // And so on,
}

func encodeWithCoder(coder: NSCoder) {
  coder.encodeObject(self.yearsToInvest, forKey: "yearsToInvest")
  // and so forth.
}

Jumping Ahead a bit...

If the user attempts saving a duplicate set of inputs/results, either by tapping the save button repeatedly, or by loading an earlier set and then saving again, it would be considerate to catch this and throw up an alert.

Rather than conforming SavedResults to the Equatable[1:1] protocol and then overloading the == operator[1:2], I opted instead to add a method to SavedResults.

SavedResults.swift
func checkEquality(against: SavedResults) -> Bool {
  let oldInputs: [Float] = [self.yearsToInvest, self.initialInvestment, self.monthlyDeposit, self.interestRate, self.additionalFeeRate, self.compoundInterval]
  let newInputs: [Float] = [against.yearsToInvest, against.initialInvestment, against.monthlyDeposit, against.interestRate, against.additionalFeeRate, against.compoundInterval]
  return oldInputs == newInputs
}

As the dateOfCalculation will hardly ever be the same, and accruedBalance is dependent upon the Float values, I decided to omit them in the check.

Now, whenever the app is asked to save the current inputs/results, a simple for-in loop can check those values against all previously saved sets.


Management

There will be multiple instances of SavedResults across the lifetime of the app. They'll be instantiated whenever the user asks to save (and it is legal to do so) and discarded when the user chooses to excise any from the saved collection. There will only be one instance of SavedResultsManager however, and it will be responsible for collecting, saving, and loading the SavedResults instances.

Its most basic implementation requires an array of the results objects and the functions save() and load() with which to archive and write (for the moment) to NSUserDefaults.

SavedResultsManager.swift
class SavedResultsManager {
  private var results: [SavedResults] = []
  private let maxNumber: Int = 15
  
  var count: Int {
    return results.count
  }
  
  init() {
    load()
  }
  
  private func save() {
    let saveData = NSKeyedArchiver.archivedDataWithRootObject(results)
    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.setObject(saveData, forKey: "saveData")
  }
  
  private func load() {
    let defaults = NSUserDefaults.standardUserDefaults()
    if let saveData = defaults.objectForKey("saveData") as? NSData {
      results = NSKeyedUnarchiver.unarchiveObjectWithData(saveData) as! [SavedResults]
    }
  }

SavedResultsManager will also utilize SavedResults' equality check.

private func duplicateCheck(newResults: SavedResults) -> Bool {
  for item in results {
    if newResults.checkEquality(item) {
      return true
    }
  }
  return false
}

So that the addResults method can now check for an empty array (for when the app first launches or after the user has deleted all saved results), an array filled to capacity (the maxNumber), or an array which contains any instance of duplicate values.

func addResults() {
  load()
  let newResults = SavedResults()
  switch count {
  case 0:
    println("Array was empty, saving")
    results.append(newResults)
    save()
  case maxNumber:
    println("Array is full")
  default:
    if duplicateCheck(newResults) {
      println("Identical results found")
    } else {
      println("Unique results, saving")
      results.append(newResults)
      save()
    }
  }
}


  1. ↩︎ ↩︎ ↩︎