A few months ago, Apple removed my app Interval RunCalc from the App Store for being too old and not updated. In this series of articles I’ll document what I did on rebuilding and improving the app. In the last installment, I hooked up my picker view interface to my app, and built the first round of user interfaces, along the way finding I’ve painted myself into a corner a few times and made several really silly mistakes. In this installment, I hook up the model so the app actually calculates something – and get a pleasant surprise.
Hook up the Model
Up to now I’ve been setting the initial data to the picker as a literal. For example in the time entry
@IBAction func timerEntry(_ sender: UIButton) { if runStats.locked != .time{ let vc = storyboard?.instantiateViewController(withIdentifier:>"InputController") as! PickerViewController vc.delegate = self vc.units = .hoursMinutesSecond vc.value = 4283.0// in seconds 3600 + 600 + 60 + 20 + 3 = 1 hr 11min 23sec vc.modalTransitionStyle = .crossDissolve present(vc, animated: true, completion:nil) } }
I set the picker to always get a 1:11:23 time. I’m going to change this to the RunStats model.
Before I do this, I notice an error I missed up to now. I have timerEntry instead of timeEntry. It’s a little thing, but it affects documentation, so I’ll fix it. Since this is an action, I unconnected the action, changed the spelling, cleaned with Command-Shift-K and then re-connect it.
Once I do that, I change the value to the runStats value for time, based on the model I created a few installments ago.
vc.units = .hoursMinutesSecond vc.value = runStats.time.seconds()
I’ll do this next for distance, which is a bit more complicated. Each of my stats in the model has a value, which has a getter for the correct units(Kilometer or mile), and a enum for the correct units for display. The picker does too, but the enum is a different type, and the value is a generic Double, getting all the unit information from the enum. I create a switch statement to convert from the model to the picker.
switch runStats.distance.displayUnits{ case .kilometers: vc.units = .kilometers vc.value = runStats.distance.kilometers() case .miles: vc.units = .miles vc.value = runStats.distance.miles() }
For pace I should have the same thing, but there a wrinkle. The picker requires a Double for all measurements. In the model I have a string for minutesSecondsPerMile. The picker is looking for a double of seconds per mile, which I don’t have in PaceStat. In the PaceStat class, I defined that string like this:
public func minutesSecondsPerMile() -> String{ let secondsPerMile = 1609.344 / paceStat let minutes = Int(secondsPerMile) / 60 let seconds = Int(secondsPerMile) % 60 return String(format:"%02i:%02i",minutes,seconds) }
Since seconds per mile was a necessary calculation to create the string, this is an easy fix. I can break that method into two methods:
func secondsPerMile()->Double{ return 1609.344 / paceStat } func minutesSecondsPerMile() -> String{ let minutes = Int(secondsPerMile()) / 60 let seconds = Int(secondsPerMile()) % 60 return String(format:"%02i:%02i",minutes,seconds) }
Then add to the paceEntry action in my view controller this switch to convert units and values for the picker.
switch runStats.pace.displayUnits{ case .kilometersPerHour: vc.units = .kilometersPerHour vc.value = runStats.pace.kilometersPerHour() case .milesPerHour: vc.units = .milesPerHour vc.value = runStats.pace.milesPerHour() case .minutesSecondsPerMile: vc.units = .minutesSecondsPerMile vc.value = runStats.pace.secondsPerMile() }
The Locked Value
Next I’ll add calculations for the locked value. This was the big surprise. I’ll add this to the RunStats model:
func recalculate() { switch locked{ case .distance: self.distance = distance(pace: pace, time: time) case .pace: self.pace = pace(time: time, distance: distance) case .time: self.time = time(pace: pace, distance: distance) } }
There are times when one over-analyzes a situation, and leads to a complex solution. I did that in version 1.0 with the calculations above. Because I planned out the model so carefully for V 2.0, this ends up incredibly easy to deal with. In the V1.0 model, I was basing things on what changed, not what was locked. Locked is my solution, not just a locked variable. It the difference between “Pace just changed! What do I do?” to “Solve for time given pace and distance” The second is a lot simpler to think about and code.
Initialization
I’ll make sure I have some starting data in view did load
override func viewDidLoad() { super.viewDidLoad() runStats.pace = PaceStat(minutes: 10, seconds: 00) runStats.pace.displayUnits = .minutesSecondsPerMile runStats.distance = DistanceStat(miles: 26.2) runStats.distance.displayUnits = .miles runStats.locked = .time runStats.time = runStats.time(pace: runStats.pace, distance: runStats.distance) updateDisplay() }
I’m ready to build and run. I’m pleasantly surprised that the first run works perfectly. The initial value loads from the model.
This is my ultimate goal for a marathon: a 4:22 finish. That’s way in the future – I’ve never run more than a half marathon. I try my last 5K run to find pace, changing the distance units, and solving for pace.
Yeah, I got a lot of training (and weight loss) ahead of me. My last long run used a slower combination of run/walk so it looks like this:
I have a ten Mile race coming up based on those paces, I can calculate My time between 2:24:50 and 2:16:50.
All of these match data calculated by other apps, so it looks like the app is working. For a change, I don’t have a lot of work to do in a development stage. I just plugged things in and they worked. I know it is supposed to work when I plan things out carefully, but it still amazes me when it does. I’ve done too much debugging to believe something actually works right the first time. In the next installment, I’ll start adding another level of complexity and that might change. I’ll start adding the interval table. First part of that phase is making a custom table view cell.
Leave a Reply