All posts by Steven Lipton

Trainer, App developer. Author. Artist. Proprietor of makeapppie.com and Host of Slice of App Pie Show

This Old App: Hooking up the Model

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.

This Old App: Adding User Interfaces

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 built a very flexible UIPickerView into my app for fast but reliable data entry. Now I have to hook up that picker view to my app, and build 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.

Back in the first installment in this series, I did a few sketches of what I wanted for the user interface. My final sketch was this:

After I made that sketch, I realized something I forgot. I’ll need a way to change units. I also want a better solution to locking one of the variables to be my result. In sketch above, that feature is scribbled in the margin. To change units, I made a button that displays units. Tapping it would give a selection of units.

I removed two of my five entries on the top, Speed and Repetition. I dropped the repetition feature completely. While writing the model, pace and speed became the same, just with different units. That left the three parts on top, a table for the intervals below it and a toolbar. I cleaned it up to look like this:

I liked the look of this, so I went to Xcode, deleted the controls on the storyboard I was using for testing and designed this:

I kept to a very simple palette. The three rows on top are UIViews, labeled Time, Pace and Speed. Each row has two or three buttons as subviews.

Using auto layout, I aligned the baselines of the of the Units  buttons( the one marked m) and Entry buttons( the one marked Distance), and aligned the Entry vertically to the view, The lock button was pinned up, down and left to 0 and I made the width of the lock button 10% the width of the view. If you need a refresher on auto layout, Check out my book Practical Autolayout for Xcode 8

I can pin all the elements where I want them. This keeps everything modular, and with no extra effort I have a landscape layout, something I never had in the original app.

In a later installment, I’ll use class sizes to make a different layout for iPad.
The lighter center section is a UIView which I use as a container view for my interval table. On the bottom is a space for the toolbar, which is not a real toolbar, but a UIView I’ll once again add buttons to. A UIView with buttons for subviews gives me greater control of the buttons than I get with a toolbar.

Once I have this on a storyboard and all my constraints in place, I add outlets and actions for all the buttons. I’m ready to code.

The Lock Button

I designed the app to input data on two of the three rows, and get a result on the third. To indicate what row will get the result, I’ll use a lock button that toggles between the rows. The selected lock button is bright yellow, the deselected buttons a dark transparent yellow. I’ll add two constants for that

let lockOffColor = UIColor(red: 1.0, green: 1.0, blue: 0.31, alpha: 0.3)
let lockOnColor = UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 0.9)

At the press of a lock button, I set the property in the model locked to the locked value, I turn on the touched button to glow yellow, and dim the other two buttons.

@IBAction func lockTime(_ sender: UIButton) {
runStats.locked = .time
timeLock.backgroundColor = lockOnColor
distanceLock.backgroundColor = lockOffColor
paceLock.backgroundColor = lockOffColor
}
@IBAction func lockDistance(_ sender: UIButton) {
runStats.locked = .distance
timeLock.backgroundColor = lockOffColor
distanceLock.backgroundColor = lockOnColor
paceLock.backgroundColor = lockOffColor

}
@IBAction func lockPace(_ sender: UIButton) {
runStats.locked = .pace
timeLock.backgroundColor = lockOffColor
distanceLock.backgroundColor = lockOffColor
paceLock.backgroundColor = lockOnColor

}

This is purely cosmetic. Figuring out what I’ll do with the lock will come later.

Setting the Unit of Measure

Each row has a unit of measure. The time row has no change of units, but the user can change the units for distance and pace. For changeable units, the button’s action will present a UIAlertController as an action sheet with possible units of measure. That will change the title of the button with the units measurement on it. Each unit of measure is a UIAlertAction with a closure. Here the distance action sheet code selecting miles and kilometers :

@IBAction func changeDistanceUnits(_ sender: UIButton) {
let actionSheet = UIAlertController(title: "Distance Units", message: "Select distance units", preferredStyle: .actionSheet)
let kilometers = UIAlertAction(title: "Kilometers", style: .default) { (alertAction) in
self.distanceUnits.setTitle("Km", for: .normal)
}
let miles = UIAlertAction(title: "Miles", style: .default) { (alertAction) in
self.distanceUnits.setTitle("Mi", for: .normal)
}
actionSheet.addAction(kilometers)
actionSheet.addAction(miles)
present(actionSheet, animated: true, completion: nil)
}

I start to get a sinking feeling here, one I had when finishing the picker as well. In version 1.0 the unit conversions were the biggest problem, and I’m beginning to wonder if they will be here too. There is nowhere in the model or controller to select the unit for distance or pace.

Opening the Entry Pickers

I’ll put that aside for the moment. Instead I’ll set up the actions for the three input buttons. In the last installment I went through the testing of a picker, and I copy that code to call the picker view as a modal. This time I’ll use a cross dissolve for the transition:

@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’m leaving the literal data in the action for now, so I’m testing known data. I’ll do similar for the distance and pace entry actions.

The Container View Table

Before I test, I want to add a little bit of code to the interval table view. I’ll implement the intervals after I get the basic app working. I’ll put some dummy data so I can look at the full User Interface. This is in a container view, a great way of getting a table view controller on a part of the superview without it using the complete view. I wrote about this more here if you are interested in the details. In short, I dragged a table view controller to the storyboard, and control-dragged from the UIView to the table view controller, selecting the Embed segue. That creates a relationship between controllers, which you see in the table view controller resizing to the size of the view.

I make a new UIViewController file. I tend not to use the UITableviewController template, since it has way too much junk in it. Instead I change the subclass UIViewController to UITableViewController.

class IntervalTableViewController: UITableViewController

Once I have this file made, I’ll use the identity inspector to set it as the class of the table view controller.

For the table view data sources I give one section and ten rows.

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}

For the cells I print the row number with alternating colors.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
if (row % 2) == 0{
cell.backgroundColor = UIColor.blue
} else {
cell.backgroundColor = UIColor.white
}
cell.textLabel?.text = " Row:\(row)"
return cell
}

I’ll add a title to the one section I have.

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Intervals"
}

The table will show up for placement, but I’ll do nothing with it in this installment. I want calculations working first.

The First Iteration Test

I’m ready for my first test. Running the app gets me this:

The table is in place and the app looks nice and clean. Tapping the m/s for meters per second, opens up a action sheet.

Tapping Miles per Hour gets me mile per hour

If I tap Time, nothing happens. That’s the lock in action. But If I tap speed, the pickers show, with the correct value.

I lock speed

and try the time:

I can send data to the pickers.

 

Matching Colors in the Picker

I’m not happy about the colors. The pickers don’t match the rest of the app. I’d like the pickers to reflect the colors of the RootViewController without me playing around too much. If I change the root’s colors later on, the colors should still match.

I set up PickerViewController for background color property observers.

var backgroundColor = UIColor.darkGray{
didSet{
view.backgroundColor = backgroundColor
}
}
var toolBarColor = UIColor.lightGray{
didSet{
toolBar.backgroundColor = toolBarColor
}
}

In RootViewController, I assign the view’s background color to the PickerViewController’s background color.

@IBAction func timerEntry(_ sender: UIButton) {
if runStats.locked != .time{
let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
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)
vc.backgroundColor = view.backgroundColor!
}
}

When I change the property backgroundColor in the picker it should change the color in the picker. I try it and still get bright backgrounds, not dark ones. I forgot to take out test code. In the PickerViewController class, I have these three functions:

func distance(){
titleLabel.text = "Distance"
backgroundColor = UIColor.cyan
}
func paceOrSpeed(){
titleLabel.text = "Pace / Speed"
backgroundColor = UIColor.yellow
}
func time(){
titleLabel.text = "Time"
backgroundColor = UIColor.green
}

I comment out those background colors and run again. The background appears.

I see two more changes. I want to change the toolbar color to the button’s color. If I change the toolbar at top to the same color as the button’s background with this:

vc.toolBarColor = sender.backgroundColor!

It fails with an unexpectedly found nil while unwrapping an Optional value error. I know what’s wrong almost immediately. This is one of those silly mistakes, but one that is so subtle it could take a long time to find it. I’m relieved I found it early. The background isn’t in the button, but in the superview of the button. The button background is transparent, hence the nil. It should be this:

vc.toolBarColor = (sender.superview?.backgroundColor!)!

That works, but the result is worse for the text. Black on dark just doesn’t work.

I’m a little nervous about all those optionals and want a function to set the the text color. I add this to clean it all up.

func colors(picker:PickerViewController, from button:UIButton){
    if let backgroundColor = view.backgroundColor{
    picker.backgroundColor = backgroundColor
}
     if let toolBarColor = button.superview?.backgroundColor{
     picker.toolBarColor = toolBarColor
}
}

I replace the lines changing color in the actions with the colors method.

colors(picker: vc, from: sender)

My foreground text solution will go in the colors function. The color I’ll choose is the same as a title color, so I can start like this:

if let titleColor = button.titleColor(for: .normal){
}

Then I get stuck. I forgot to add a foreground color property to PickerViewController. The UIPickerView in the controller also does not have a foreground color property to set. Originally, I used the easiest of the UIPickerViewDatasource methods to display a picker component pickerView:titleForRow:forComponent. That returns a string. There’s a delegate method to return a UIView, such as a UILabel in whatever color or font I want:

func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    let label = UILabel()
    label.font = UIFont.preferredFont(forTextStyle: .title1)
    label.textColor = textColor
    switch components[component]{
    case .ten:
        label.text = String(format: "%01i", row)
    case .six:
        label.text = String(format: "%01i", row)
    case .colon:
        label.text = ":"
    case .decimal:
        label.text = "."
    }
    return label
}

I used a property textColor which I have yet to define. I’ll use a property observer to change all the other text on the picker controller to this color.

var textColor = UIColor.black{
    didSet{
        backButton.setTitleColor(textColor, for: .normal)
        doneButton.setTitleColor(textColor, for: .normal)
        titleLabel.textColor = textColor
        unitsOfMeasureLabel.textColor = textColor
}
}

Back in RootViewController, the colors method changes to:

func colors(picker:PickerViewController, from button:UIButton){
     if let backgroundColor = view.backgroundColor{
         picker.backgroundColor = backgroundColor
}
     if let toolBarColor = button.superview?.backgroundColor{
         picker.toolBarColor = toolBarColor
}
     if let titleColor = button.titleColor(for: .normal){
         picker.textColor = titleColor

}
}

Now I reflect the colors in my buttons in the picker, giving a consistent and readable look:

The title should read Distance, not 583.0 . That’s a testing element I left in the title, which I comment out.

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
updatePickerValue()
//titleLabel.text = "\(value)" //for testing only
}

Delegation From the Picker

I’ll send data from the picker to the root via delegation. I’ll add to PickerViewController.swift a protocol for a delegate:

protocol PickerViewControllerDelegate{
func pickerDidFinish(value:Double, controller:PickerViewController)
}

I’ll go through all the normal steps for making a delegate. If you don’t know those I’d suggest my LinkedInLearning course on delegation and datasources. The last of those steps is adding the delegate method to RootViewController.

func pickerDidFinish(value: Double, controller: PickerViewController) {
}

I’ve been ignoring an important question: value is a Double, but what does it measure? What’s the units? How do I reflect that in the user interface?

I designed PickerViewController with an enum for that.

enum Units {
        case meters,kilometers,miles //distance
        case milesPerHour,minutesSecondsPerMile,kilometersPerHour // speed and pace
        case hoursMinutesSecond //time
    }
}

I set up a switch clause, and assign value to the right place in the right unit system…well almost.

func pickerDidFinish(value: Double, controller: PickerViewController) {
    switch controller.units {
    case .meters:
        runStats.distance.meters(value)
    case .kilometers:
        runStats.distance.kilometers(value)
    case .miles:
        runStats.distance.miles(value)
    case .hoursMinutesSecond:
        runStats.time.seconds(value)
    case .kilometersPerHour:
        runStats.pace.kilometersPerHour(value)
    case .milesPerHour:
        runStats.pace.milesPerHour(value)
    case .minutesSecondsPerMile:
        //ummmmm.......not there!!!
    }
}

I need one more conversion setter I didn’t originally have in PaceStats. for .minutesSecondsPerMile going to the picker, I have seconds per mile coming back out, but no method to change and store that as meters per second. I did the calculations and included it as part of the minutes and seconds getter:

public func minutes(_ minutes:Int,seconds:Int){
let secondsPerMile = Double(60 * minutes + seconds)
paceStat = 1609.344 / secondsPerMile
}

I can break this up to

func secondsPerMile(_ secondsPerMile:Double){
paceStat = 1609.344 / secondsPerMile
}
public func minutes(_ minutes:Int,seconds:Int){
secondsPerMile(Double(60 * minutes + seconds))
}

then finish the delegate method with

case .minutesSecondsPerMile:
runStats.pace.secondsPerMile(value)

Display Updates

I returned data from the picker and changed the model. Next I’ll update the root view’s display with new values. I’ll start a new method and add this for the time using the hourMinutesSeconds function of TimeStat

func updateDisplay(){
//time
    let timeTitle = "Time: " + runStats.time.hoursMinutesSeconds()
    timeEntry.setTitle(timeTitle, for: .normal)
}

For distance, I have meters, kilometers and miles. Which one do I display? I have no idea and the model doesn’t either. I’ll have to add tracking of this. I can track this in the model or in the controller. I go for the model. I’ll add an enumeration, a property using that enumeration, and a method that returns a formatted string based on that property.

I’ll try this first with the simpler DistanceStat. The enum for distance DisplayUnits would be kilometers and miles, which I add to a property displayUnits.

enum DisplayUnits {
    case kilometers,miles
}
var displayUnits:DisplayUnits = .miles

To my getters, I add the method

func displayString()->String{
    let string = "Distance: %7.1"
    switch displayUnits {
    case .miles:
         return String(format:string, miles())
    case .kilometers:
        return String(format:string, kilometers())
}

}

With a model that tracks the units I’m using, I set displayUnits in the action sheets’ alert actions I set up earlier. I’ll also update the display when I change units. For distance’s kilometer Alert Action it would look like this:

let kilometers = UIAlertAction(title: "Kilometers", style: .default) { (alertAction) in
self.distanceUnits.setTitle("Km", for: .normal)
self.runStats.distance.displayUnits = .kilometers
self.updateDisplay()
}

Miles would follow the same pattern. I‘ll set the distanceEntry button title in updateDisplay to

distanceEntry.setTitle(runStats.distance.displayString(), for: .normal)

In distanceEntry, I’ll set vc to 5 kilometers as the initial value. I’ll hit run and get 5 kilometers.

I’ll change that to 8 and press Done. Nothing happens. That’s because I forgot to add update to the delegate method. I did add it to the units, so I’ll change to Kilometers and get 7, which is the wrong answer.

I change to Miles and also get 7.

Checking my code I find a seriously stupid error.

let string = "Distance: %7.1”

I forgot an f. It should be %7.1f. I add updateDisplay to the delegate method too. I try again. And this time it works. For kilometers I get 5.0 and for miles 3.11.

I’ll do the same thing for the pace, now that I know this works. The only variation is distinguishing between speed and pace, which I do in updateDisplay.

var paceTitle = runStats.distance.displayString()
if runStats.pace.displayUnits == .minutesSecondsPerMile{
    paceTitle = "Pace: " + paceTitle
} else {
    paceTitle = "Speed: " + paceTitle
}
paceEntry.setTitle(paceTitle, for: .normal)

I try that out in the simulator, and it fails, reading zero. The picker reads correctly as 11:23 min/mi and the speed/pace label changes correctly. Changing the units always reads zero. I add to the delegate method a print(value) to check if the value gets there, and it does. Still in the delegate method I try to see if the displayString() works right, and it does. The problem is in updateDisplay, and once again it’s a typo. I had runStats.distance instead of runStats.pace.

I try it one more time, And this time it works.

I’ve connected all the user interface elements to the controller. Along the way, I made several silly mistakes, which can and will happen to anyone writing code. Its silly mistakes like these that make the most bugs. I also gained a greater appreciation for enum and switch, which are doing an incredibly good job of keeping my units straight. I have a consistent user interface, one that I find streamlined and elegant, at least compared to version 1.0. I have a lot to go though. In the next installment, I’ll hookup the model to the entry buttons, and start calculating run stats.

This Old App: Why I Hate PickerViews, but Learned To Love Them.

A few months ago, Apple removed my app Interval RunCalc from the App Store for being too old. In this series of articles I’ll document what I did on rebuilding and improving the app. In the last installment, I placed my new model into the old app, fixed a lot of errors from taking 2013 Objective-C code in 2017’s Xcode and make a quick, temporary view and controller to test it. Next, I’ll replace text fields I used for input. My best choice for this bothers me. UIPickerView is one of my biggest love/hate relationship in all of iOS development.

The app from my last installment looks something like this:

While this is okay for testing the model, it is not something I’d want for users. There’s a principle I’d like to introduce: Get Good, Get Fast. When working with user interfaces, get the data from your user as quickly as possible. The more time it takes a user to input data, the less likely he or she will use the app. They will get frustrated. What they enter must be good data. Data must makes sense to the application. We refer to this as valid data.

The current user interface is fast since I can type on the keyboard, but fails miserably in the validation arena. Because I’m using a keyboard and my values from the keyboard are strings, there’s a lot of data validation code I need to be sure I’m getting good data from the user.

In the version 1.0 of Interval RunCalc, I used keyboards. I tried controlling data validation by breaking up numbers into parts. For example, time used a special controller that broke up time into three integers were easier to input but restricted themselves to certain values

I did this for HH:MM:SS, MM:SS and decimal values. Problem was I had those values everywhere in the application, so my storyboard ended up a huge mess of segues.

My rationale at the time was simple: I didn’t want to use a UIPickerView for input. I hated UIPickerViews. There’s two reasons I hate them. Picker views break the Get Fast Get Good principle on the get fast side. They are effective for no more than ideally ten to twenty items. The classic picker view was one like this with 100 components.

It takes forever to get to higher numbers, breaking get fast. I get frustrated with these in other apps, and did not want to subject my users to such a thing.

The other objection separates new developers from seasoned ones. New developers, upon learning about picker views want to abuse them. They’ll put everything in picker views, and several on the same view. They’ll throw insane amounts of data in the most confusing ways, often making the UI unusable. For example here’s six components and two picker views. You can only read it in landscape

This would take forever to input data and gets very confusing fast. I get more weird questions about bizarre uses for picker views than any other control. Most of these developers do not think like users. Users hate picker views because they are confusing and take forever to use.

There is one strength for UIPickerView: Validation. UIPickerViews constrain input. Apple uses this for the date picker, taking four elements and combining them to get a Date value.

This is how picker views are supposed to be used. But even here I hate anything over 20 rows, like the date.

The date picker gives us the clue of how to use them right: Separate components into discrete elements than can have only constrained values. Make those elements with as few elements as possible. I set a limit of twenty rows. The keyboard input was a horrible mess in version 1.0. It never worked right. Despite my objections to them, I decided to use a picker view for my input to avoid all the data validation. I want one class of UIPickerView controllers. It will adapt to the different scenarios I need: HH:MM:SS, HH:MM:SS, and two digit decimal. I’ll make this a modal view instead of the navigation view I used in the original application to get rid of all those segues. I wrote my storyboard for it, which I placed on my Main.Storyboard:

This looks like a navigation controller, but it is an independent view controller on my storyboard, which will be a modal controller. I set a storyboardID of InputController. I made an illusion of a toolbar from two buttons, a label, and a UIView behind them, arranged with some auto layout.

This is a very flexible input system, giving the user the units I’m using and giving me power over all the buttons and labels. I’ll make a view controller PickerViewController for it, and I’ll add the following outlets and actions to that view controller:

@IBOutlet weak var backButton: UIButton!
@IBOutlet weak var doneButton: UIButton!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var toolBar: UIView!
@IBOutlet weak var unitsOfMeasureLabel: UILabel!
@IBOutlet weak var picker: UIPickerView!

@IBAction func didPressDone<span class="hljs-params">(_ sender: UIButton) {

}

@IBAction func didPressBack<span class="hljs-params">(_ sender: UIButton) {

}


Adding the Picker View 

In this article I’m more interested in the application than the implementation of a picker view. I’ll be leaving out steps. If you need to understand picker views and picker components, I’ve written out the whole process in detail which you can find here. I’ll be making a variation of that code, but understanding the way I did that will help you here.

My picker view is based on digits. No component will have more than 10 rows, speeding up the input process. Looking at the types of components I have:

  • Decimal digit – values of 0,1,2,3,4,5,6,7,8,9
  • Tens of minutes and seconds – values of 0,1,2,3,4,5
  • Time separator – a colon(:)
  • Decimal separator – a period(.)

I could add constants for this to my code, making four types of component wheels to add to my code and making some string arrays to do this.

let decimalDigit = ["0","1","2","3","4","5","6","7","8","9"]
let timeDigit = ["0","1","2","3","4","5"]
let timeSeperator = [":"]
let decimalSeperator = ["."]

That’s what I did in the article I mentioned above. There is another way to do this based on the row number and a bit of control logic. Instead of constants, I’ll use an enumeration.

enum ComponentType {

&nbsp;&nbsp;&nbsp;&nbsp;case ten,six,colon,decimal

&nbsp;&nbsp;}

I’ll make an array of this enum.

private var components = [ComponentType]()

This way I can describe any set of wheels quickly. MM:SS is [.six,.ten,.colon,.six.ten].The number of components are the size of this array

func numberOfComponents(in pickerView: UIPickerView) -&gt; Int {

&nbsp;&nbsp;&nbsp;&nbsp;return components.count

&nbsp;&nbsp;}

For HH:MM, this would return 5. I’ll set the number of components in my datasource, based on the component type:

func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    switch components[component]{
        case .ten: return 10
&nbsp;&nbsp;&nbsp;&nbsp;    case .six: return 6
&nbsp;&nbsp;&nbsp;&nbsp;    case .colon,.decimal: return 1
&nbsp;&nbsp;  }
}

Since I’m not using a string array to give me the title for a row in a component, I use the row number. The digit I want to display is equal to its row number. Make a formatted string of the row number. The exceptions are colons and decimal points, which I’ll explicitly return. I display the correct digit like this:

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
&nbsp;&nbsp;&nbsp;&nbsp;switch components[component]{
&nbsp;&nbsp;&nbsp;&nbsp;    case .ten: return String(format: "%01i", row)
    &nbsp;&nbsp;&nbsp;&nbsp;case .six: return String(format: "%01i", row)
    &nbsp;&nbsp;&nbsp;&nbsp;case .colon: return ":"
    &nbsp;&nbsp;&nbsp;&nbsp;case .decimal: return "."
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}&nbsp;&nbsp;

This all depends on the correct values in the components array. I’ll add another enum, listing input formats. I have three formats for that enumeration: a double with two dight decimal, MM:SS and HH:MM:SS.

enum PickerType {

&nbsp;&nbsp;&nbsp;&nbsp;case twoDecimal,minuteSecond,hourMinuteSecond

&nbsp;&nbsp;}

I’ll write a function to set the components.

&nbsp;func setPickerComponents(for pickerType:PickerType){
&nbsp;&nbsp;&nbsp;&nbsp;self.pickerType = pickerType

&nbsp;&nbsp;&nbsp;&nbsp;switch pickerType{
&nbsp;&nbsp;&nbsp;&nbsp;    case .twoDecimal:<span class="hljs-comment">//999.99 max value
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     components = [.ten,.ten,.ten,.decimal,.ten,.ten]&nbsp;&nbsp;&nbsp;&nbsp;
        case .minuteSecond: <span class="hljs-comment">//59:59 max value
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     components = [.six,.ten,.colon,.six,.ten]
&nbsp;&nbsp;&nbsp;&nbsp;    case .hourMinuteSecond: <span class="hljs-comment">//99:59:59 max value
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;components = [.ten,.ten,.colon,.six,.ten,.colon,.six,.ten] &nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;}

The class has two data properties. When I present the view controller, I’ll set these two properties.

var units = Units.meters

&nbsp;var value = 0.0&nbsp;&nbsp;

One is the units of measure and the current value. Units is another enumeration which lists all the app’s units of measure:

enum Units {
&nbsp;&nbsp;&nbsp;&nbsp;case 
       meters,kilometers,miles,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;milesPerHour,minutesSecondsPerMile,kilometersPerHour,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hoursMinutesSecond
&nbsp;&nbsp;}

There’s a lot of cosmetic changes I’ll make depending on the units, such as displaying the units and if this is a speed, distance or time variable. I wrote a method configure to handle all this, leaving some methods to do a lot of the repetitive configuration.

func configure<(for units:Units){

&nbsp;&nbsp;&nbsp;&nbsp;switch units{
&nbsp;&nbsp;&nbsp;&nbsp;case Units.meters:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Meters"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.kilometers:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Kilometers"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.miles:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Miles"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;distance()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.milesPerHour:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Miles per Hour"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.minutesSecondsPerMile:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setMinutesSeconds()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Minutes per Mile"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.kilometersPerHour:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTwoDecimal()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Kilometers per Hour "
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;paceOrSpeed()
&nbsp;&nbsp;&nbsp;&nbsp;case Units.hoursMinutesSecond:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setHoursMinutesSeconds()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unitsOfMeasureLabel.text = "Time"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;time()
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}

I’ll add three methods for configuring the types.

func setTwoDecimal<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .twoDecimal)
}

func setHoursMinutesSeconds<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .hourMinuteSecond)&nbsp;&nbsp;
} &nbsp;

func setMinutesSeconds<span class="hljs-params">(){
&nbsp;&nbsp;setPickerComponents(for: .minuteSecond)
}


For the user interface side of things, I set the background color and title depending on the units so users have visual cues:

func distance(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Distance"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.cyan
&nbsp;&nbsp;}

&nbsp;&nbsp;func paceOrSpeed(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Pace / Speed"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.yellow
&nbsp;&nbsp;}

&nbsp;&nbsp;func time(){
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "Time"
&nbsp;&nbsp;&nbsp;&nbsp;backgroundColor = UIColor.green
&nbsp;&nbsp;}&nbsp;&nbsp;

The First Test

I have enough code to test the picker now. I’ll set up a button on my test storyboard and add some code to configure the picker. I’ll add a button Picker.

I’ll make an action for this button in RootViewController.swift. This input controller is intended for modal controllers. Modals launching for a storyboard identifier makes sense in this context. I use it multiple times. It clears all that segue spaghetti I had in version 1.0. I’ll launch the controller in a modal view using the default values.

@IBAction func pickerButton<span class="hljs-params">(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: <span class="hljs-literal">true, completion: <span class="hljs-literal">nil)
&nbsp;&nbsp;}

I’ll go back to the PickerViewController.swift file and add dismissal code in my actions.

@IBAction func didPressDone(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;self.dismiss(animated: true, completion: nil)
&nbsp;&nbsp;}

@IBAction func didPressBack(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;self.dismiss(animated: true, completion: nil)
&nbsp;&nbsp;}

One thing I forgot – to call configure. I’ll do that on the viewDidLoad of PickerViewController.swift

override func viewDidLoad() {
&nbsp;&nbsp;&nbsp;&nbsp;super.viewDidLoad()
&nbsp;&nbsp;&nbsp;&nbsp;picker.dataSource = self
&nbsp;&nbsp;&nbsp;&nbsp;picker.delegate = self
&nbsp;&nbsp;&nbsp;&nbsp;configure(for: self.units)
&nbsp;&nbsp;}

I’m ready to run. When I build and run, then press the picker button, I get a beautiful interface, where I can roll the individual digits.

I notice a potential bug. I ran a quarter of a marathon or 10.56 Km last weekend. That’s 10557.30 meters, which I could not enter into the app since I don’t have enough digits. I could add another two digits for 99999.99 for meters, but before I do, I’ll ask myself the question: will the user enter anything in meters?

This app is for distance runners. Distances are in kilometers or miles, never meters. That would be a change that has no use. I only care about what the user wants here, not my internal measurement system of meters. I’ll skip that change.

I do another test. I add the unit of measure in the action in RootViewController for a pace.

@IBAction func pickerButton(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = .minutesSecondsPerMile
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: true, completion: >nil)
&nbsp;&nbsp;}

Building and running again, I can spin the wheels there too.

Setting Values

I want to set the picker to an initial value. Inside the picker’s code I‘ll change value to a series of row indices representing the number, one digit per component. I’ll convert value to a string, then use the characters to represent rows.

If I had a 11:23 minute/mile setting, I set the string to "11:23" The code looks at a character from the left to the right as the row index for the component. The functions Int(String(character)) get me this index used in a for loop. I use the selectRow method to select the row with that number.

picker.selectRow(row, inComponent: component, animated: animated)

The Int constructor produces a nil value for any character that isn’t a digit. Those are my separators, and they always have a row value of 0. The code looks like this:

func setComponentValues(from str:String){&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;var component = 0
&nbsp;&nbsp;&nbsp;&nbsp;for character in str.characters{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if let row = Int(String(character)){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectRow(row, inComponent: component, animated: animated)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectRow(0, inComponent: component, animated: animated)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;component += 1
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}

I made three functions to set the value depending on the PickerType. If their sparse code didn’t make sense before, they will now. Each converts the value to a string for that PickerType, then converts that string to the correct rows in the picker components

func setTwoDecimal(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .twoDecimal)
&nbsp;&nbsp;&nbsp;&nbsp;let str = String(format:"%06.2f",value)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;func setHoursMinutesSeconds(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .hourMinuteSecond)
&nbsp;&nbsp;&nbsp;&nbsp;let hours = Int(value + 0.5) / 3600
&nbsp;&nbsp;&nbsp;&nbsp;let hoursRemainder = Int(value + 0.5) % 3600
&nbsp;&nbsp;&nbsp;&nbsp;let minutes = hoursRemainder / 60
&nbsp;&nbsp;&nbsp;&nbsp;let seconds = hoursRemainder % 60
&nbsp;&nbsp;&nbsp;&nbsp;let str =&nbsp;String(format:"%02i:%02i:%02i",hours,minutes,seconds)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;func setMinutesSeconds(){
&nbsp;&nbsp;&nbsp;&nbsp;setPickerComponents(for: .minuteSecond)
&nbsp;&nbsp;&nbsp;&nbsp;let minutes = Int(value) / 60
&nbsp;&nbsp;&nbsp;&nbsp;let seconds = Int(value) % 60
&nbsp;&nbsp;&nbsp;&nbsp;let str = String(format:"%02i:%02i",minutes,seconds)
&nbsp;&nbsp;&nbsp;&nbsp;setComponentValues(from: str)
&nbsp;&nbsp;}

&nbsp;&nbsp;

To save myself some time I copied and pasted some code from my model to make the time strings.

In RootViewController, Add a value to the action:

@IBAction func pickerButton(_ sender: UIButton) {
&nbsp;&nbsp;&nbsp;&nbsp;let vc = storyboard?.instantiateViewController(withIdentifier: "InputController") as! PickerViewController
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = .minutesSecondsPerMile
&nbsp;&nbsp;&nbsp;&nbsp;vc.units = 683.0 // in seconds 600 + 60 + 20 + 3 = 11min 23sec
&nbsp;&nbsp;&nbsp;&nbsp;present(vc, animated: <span class="hljs-literal">true, completion: <span class="hljs-literal">nil)
&nbsp;&nbsp;}

I’ll test this and see 11:23

Getting Values

The purpose of a input picker is the get a value from the user. To do that I’ll update value every time the picker changes. I’ll set up the picker’s delegate function to update the value in a function

&nbsp;func pickerView<span class="hljs-params">(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
&nbsp;&nbsp;&nbsp;&nbsp;updatePickerValue()
&nbsp;&nbsp;&nbsp;&nbsp;titleLabel.text = "<span class="hljs-subst">\(value)" //for testing only
&nbsp;&nbsp;}

I added a extra line for testing the value, sending the value to the title. In the app we’ll use a delegate, but that’s for the next phase, so I’ll display it here for now.

The the updatePickerValue function converts the components into a value. I built this into another switch pickerType control structure. This is the opposite of the setting the value. Take the row and convert it to a value. If it is part of a bigger number, add it with the other digits of the number. For example, for the .twoDecimal, I did this:

case .twoDecimal:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;picker.selectedRow(inComponent: 5)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let hundredth = Double(picker.selectedRow(inComponent: 5))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let tenth = Double(picker.selectedRow(inComponent: 4))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let fraction = tenth * 0.1 + hundredth * 0.01
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let ones = Double(picker.selectedRow(inComponent: 2))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let tens = Double(picker.selectedRow(inComponent: 1))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let hundreds = Double(picker.selectedRow(inComponent: 0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let wholeNumber = hundreds * 100 + tens * 10 + ones
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value = wholeNumber + fraction

I did this explicitly since it is easier to debug than an algorithm for it. I do the same for the time-based picker types, grabbing a digit and adding it all together.

&nbsp;case .minuteSecond: //00:00 returning seconds
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var tens = Double(picker.selectedRow(inComponent: 0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var ones = Double(picker.selectedRow(inComponent: 1))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let minutes = (tens * 10 + ones) * 60
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tens = Double(picker.selectedRow(inComponent: 3))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ones = Double(picker.selectedRow(inComponent: 4))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let seconds = (tens * 10 + ones)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value = minutes + seconds&nbsp;&nbsp;&nbsp;

Run again and I’ll change the pace to 10:23. As expected, 623.0 is in the title.

 

I’ve built a decent picker that follows Get Good, Get Fast. All data in and out of the picker is a single value of double. What that value represents is a unit of measure. My unit of measure configures my picker for the only legitimate values. I call the picker from a modal view programmatically without a lot of confusion in the storyboard. My next step is the user interface, and hooking this up to it. In the next installment, I’ll build that UI and find I made a few bad assumptions with the picker and find some other interesting dilemmas.

This Old App: 2013 Code, Meet Xcode 8

A few months ago my app Interval RunCalc was removed 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 our first article I analyzed the changes necessary. In the second, I built the model in a Swift playground. Now it is time to place that model into the old app, and make a temporary view and controller to test it.

Open the App in Xcode

I made a copy of the original app development files, then opened them in Xcode. The issue navigator started complaining right away:

Clicking on the issue, I find several updates Xcode wanted to make.

I select Perform Changes resolving the issues. I find in the target settings for the project, there’s more issues: provisioning profiles for debug and release.

>

Above the issue, I’ll find a checkbox Automatically Manage Signing. I check that and enable the automatic signing.

I find provisioning one of the most irritating parts of working with Xcode. Anything to get me away from it is a good thing.

Of course that’s not the end of the issues. I get another error message.

If I manage singing automatically, I have to pick the team. I only have me so I pick myself from the drop down. The target is set up, and Xcode is happy again.

Configuring the New Version

This is a major version update for the app. I want to change the version number to version 2.0. Originally I wanted the app to be called runner’s calc, but that name was taken. I settled on the mouthful Interval RunCalc. That’s too long for an icon, I want to change the Display Name to a shorter name, Run Calc to show better on an icon.

The next change I’ll make is the deployment info. Currently, the deployment is iOS 8.0 and only on iPhone in portrait.

Which iOS version to set to is a good question. In most cases, you want to deploy a version or two back for compatibility. Here’s where getting user data comes in handy. I’ll use iTunes Connect’s analytics, and look at the analytics for 2016.

Two active devices per month means there’s almost no one but me using this. Usage data strongly hints me that the biggest user is me. The large peaks occur at races I either ran or was a spectator (marathons are my 2018 goal). While a second user might be at the same races, I find it unlikely.

I’m looking at a sample that’s less than a third of total apps out there. Still, upping the version to 10.3 is not tragic since it looks like nobody but me uses this. I’ll bump it up all the way for the revision. I’m also going to make this a Universal app instead of a iPhone app. My new settings look like this:

Later, I’ll set the iPad and iPhone interface to the same initial storyboard.

The Old Structure

Opening the project navigator, You can see how I put together this app:

You’ll see the root files are the RunnersCalc files. The top level contains the initial view controller and the help controller. There was a help controller, though no one knows how to get to it — I unfortunately hid it too well. If you got to it, it was just written instructions, and very badly written. It’s another thing to go on the list of changes.

The folders under this root level has one folder for the model, and three folders for the view controllers: Intervals, Input controls and Splits.

For those from a completely Swift background, those .h and .m files are Objective-C files. The .h files contain a header which indicates what properties and methods are sharable to the rest of the app. Your code is in the .m files.

Opening up the Model folder and clicking the PCPace.h file, I find the declarations for the model, the properties of the model were:

//properties of the object

@property NSTimeInterval pace; // seconds per mile

@property float speed; // miles per hour

@property float distance; //miles

@property NSTimeInterval time; //seconds of the run or split

@property NSTimeInterval splitPace; //seconds per mile or km

@property int splitCount; //total number of splits

@property int splitNumber; //current split -- necessary?

@property int fullSplitCount; //number of splits for the full mile/kilometer

@property NSTimeInterval elapsedTime; //elapsed time in a split

@property float totalDistance; //totalDistance to this point

enum PCVariable {

  kcPCSpeed,kcPCDistance,kcPCPace,kcPCTime

};

@property enum PCVariable finding;


Add the Playground Code to the App

I’m not too worried about any of this code, since I’m about to replace it all. I open up the RunStats playground so I can copy my code. I have the code stored in four files:

In the App project, I’ll make four new files in the model folder. I’ll select the Model Folder first to keep the group correct as I add files. I’ll hit Command-N to make a new file. There’s several ways I can make these files, since I’m cutting and pasting everything into the file. I’ll select Swift File for the template, though I could have just as easily picked a Cocoa Touch Class subclassing NSObject.

I’ll be asked to save with a file name. I’ll call this one RunStats.

On the bottom, I’ll make sure the target is correct and the Group is Model

We click Create, and get a new Message:

I usually create the bridging header. As the message says the bridging header opens up the classes, methods and properties in the Objective-C code to the the newer Swift code if you want to use it. In this application I won’t use it. However, it is not a lot of overhead. I’m planning to scrap all the Objective-C, but I never know in an intermediate step if I’ll need it, so I’ll create it anyway.

I’ll repeat this three more times for the classes DistanceStat, PaceStat and TimeStat. I have two classes in the RunStats playground page. I’ll add one more for RunStatsIntervals, so every class has its own file.

I’ll organize this a little better. I’ll select my Objective-C files PCPace.h, PCPace.m, and the bridging header. I’ll right click and make New Group From Selection, naming the group Objective-C. I’ll do the same for the Swift code, selecting it all and making a Swift group for it.

I’ll select RunStats, and see this:

//

// RunStats.swift

// RunnersCalc

//

// Created by Steven Lipton on 4/6/17.

// Copyright © 2017 Steven Lipton. All rights reserved.

//



import Foundation


I’ll delete the import foundation, and then copy and paste my RunStats class from the playground to the App, leaving the intervals class and the test data in the playground. I’ll make sure I copy the import UIKit when I do. It will of course have lots of errors for the moment. We have the three classes it refers to yet to add. I’ll do the same copy and paste to the other four files, adding the classes and import UIKit in each case.

Once I’m done with this there are seven issues to solve. When transferring a playground, you’ll probably get these errors.

If you click on the first error, it will show you this in the editor:

The time.seconds() and the distance.meters() lines Xcode wants to assign to something. I used them to get a quicklook for debugging the playground. All the errors are like this. The simple solution is delete that code. The fatal error is the same thing. it’s on the line

 self.pace

It was to get a quicklook for the object in the playground. I deleted it to clear the error.

A New View and Controller

The new model is in the app, and I’d like to test it. Right now, this code is ignored by the app. What I’ll do is set up a new storyboard to check the model. In the Storyboards group, I have a blank iPad storyboard and the Main_iPhone.storyboard file, which has the current storyboard for the app:

This is a big mess. This is one of those times when doing an update project like this you just shake your head and wonder what you were thinking back then. With a little investigation, you’ll find some of those answers. I iterated this model with a inadequate model to start, and I needed a lot of UI to cover up my mistakes. The major reason this app never had an iPad version was I’d have to replicate this storyboard on an iPad.

The new app had a completly new Storyboard. I’m using auto layout to need one storyboard for all devices. I Press Command-N, selecting a Storyboard object and name it Main, making sure my Group is Storyboards.

The storyboard I get is completely blank. I’ll drag a view controller onto the storyboard

I’ll change the attribute to give this a name and set this view controller to Is Initial view controller.

I’ll need a ViewController file for this scene. I’ll press Command-N again, click a Cocoa Touch Class for the template and make the Class RootViewController, subclassing UIViewController and Swift as the language.

When I save the file I’ll save it to the RunnersCalc root folder, where the root and help view controllers are. Going back to the main storyboard, I’ll set the Custom Class to RootViewController. Now I’m ready to add some controls.

This first layout is only for testing purposes. No auto layout just yet, just drag and drop. I’ll add a label for a result and a button to calculate the result. I’ll use text fields for input, so I’ll need room for the keyboard.  I’ll set the button at the center of the iPhone screen, giving the keyboard I hope plenty of room, and the result at the top. I’ll make the label read Result and the button Calculate

I Drop two text fields between the label and the button, which I’ll call variable 1 and 2. That will be for decimal values. For time values I’ll add three textfields under those two text fields. The layout looks like this, using placeholder text for titles.

Next, I’ll wire up my view controller. I’ll open the assistant editor. All but the button are outlets, So I’ll do those first, control dragging into the view controller.

@IBOutlet weak var result: UILabel!

  @IBOutlet weak var variable1: UITextField!

  @IBOutlet weak var variable2: UITextField!

  @IBOutlet weak var hours: UITextField!

  @IBOutlet weak var minutes: UITextField!

  @IBOutlet weak var seconds: UITextField!

I’m keeping this very generic for a reason. This is a test bed. I’ll use these same fields to check different parts of the model. I’ll calculate the values in code in the calculate button, which I now wire up as an action.

@IBAction func calculate(_ sender: UIButton) { 

  }

There’s a function I’ll need in the view controller. It will take a TextField and convert the text in it to a Double. I can close up the storyboard and assistant editor, and then add the function to RootViewController

  func double(textField:UITextField!)->Double{

    guard let textField = textField 

       else {return 0.0} //uwrap textfield

    guard let value = Double(textField.text!) 

       else {return 0.0} //find double

    return value

  }

This will give me a value of 0 for anything that isn’t a true Double, and return a Double if it is. There’s three optionals, and I deal with all the unwrapping here giving me a value or a zero. This is not something I’ll need in the finished code. The input system (which we cover next time) will restrict the user to only valid values.

With the structure of the test bed done, I’ll add the model to the code.

 let runStats = RunStats()

I’ll test the function the IBAction calculate. I’ll start with a simple test to make sure everything works, converting kilometers to miles.

@IBAction func calculate(_ sender: UIButton) {

    let distance = double(textField: variable1)

    runStats.distance.distance(kilometers: distance)

    let resultText = String(format: "%02.2d", runStats.distance.miles())

    result.text = resultText

  }


I use the double function to turn the variable1 text field into a double, then add it to the model with a kilometer measure. I change the distance to miles in a string, placing the result in result.

I’m almost ready to run this code, but there’s one more thing I need to do: Change the initial storyboard. In the General tab of the Target Properties, you’ll find this:

When run, the app goes to the old Main_iPHone storyboard, not the new Main. I also restricted the layout to portrait in the old version, and I’ll use all orientations for the new version. I set both the iPad and iPhone to the same storyboard Main:

Setting my simulator to an iPhone 7 I run the app. Two things happen. I get 44 warnings, but the app runs. I try putting in a value. I find I didn’t leave enough room for the keyboard

I’ll stop the app and move the calculate button up. I try running again. I’ll type in 5 when the calculator appears, and get rewarded with a value of…Zero instead of 3.11.

I first check to see if the model is working right with a few print statements

@IBAction func calculate(_ sender: UIButton) {

    let distance = double(textField: variable1)

    print("distance in km \(distance)")

    runStats.distance.distance(kilometers: distance)

    print("distance in km \(runStats.distance.kilometers())")

    print("distance in km \(runStats.distance.miles())")

    let resultText = String(format: "%02.2d", runStats.distance.miles())

    result.text = resultText

  }

When run, I get output on the console:

distance in km 5.0

distance in km 5.0

distance in km 3.10685596118667

The model is working fine. My bug is a silly one of setting the format wrong in the string. I change the format to this:

let resultText = String(format: "%6.2f", runStats.distance.miles())

When I run again, the test works.

I go back to the storyboard. That alphanumeric keyboard is annoying. For all the TextFields, I set the keyboard type to decimal pad.

I’m ready to try another test. Let’s take distance in miles and a speed in miles per hour and calculate time. I’ll convert distance and speed to Doubles, then set them to the runstats model. I’ll try this code.

@IBAction func calculate(_ sender: UIButton) {

    let distance = double(textField: variable1)

    let speed = double(textField: variable2)

    runStats.distance.distance(miles: distance)

    let pace = PaceStat(milesPerHour: speed)

    runStats.time(changedBy: pace)

    result.text = runStats.time.hoursMinutesSeconds()

  }  

I try a simple case where I run five miles at five miles an hour, And yes, I get the expected one hour.

I try a function with a time variable using the three textfields on the bottom. I’ll compute the pace in minutes per mile for a given distance.

@IBAction func calculate(_ sender: UIButton) {

    runStats.distance = DistanceStat(miles: double(textField: variable1))

    let seconds = Int(double(textField: self.seconds))

    let minutes = Int(double(textField: self.minutes))

    let hours = Int(double(textField: self.hours))

    let time = TimeStat(hours: hours, minutes: minutes, seconds: seconds)

    runStats.pace(changedBy: time)

    result.text = runStats.pace.minutesSecondsPerMile()

  }

I build and run this. A simple test is a one hour run of five miles, which should give us a twelve minute mile.

And my target time for my first marathon of five hours 45 minutes ( yeah, I’m a bit slow)

As expected, I get the 13:10 pace. I’d do a lot of tests like this to make sure the components work with the model.

A Model Change

During the testing, I get the feel for using the code in the model. There’s one thing I don’t like. Consider these two statements

runStats.distance.distance(miles: 5.0)

print(runStats.distance.miles())

I use different identifiers for the setter and getter. I like this better:

runStats.distance.miles(5.0)

print(runStats.distance.miles())

It is easer to read and shorter to type. It is not a huge change in the model code, and it works better. I can change the setters to this:

//set distance from miles

  public func meters(_ meters:Double){

    distanceStat = meters

  }

  public func miles(_ miles:Double){

    distanceStat = miles * 1609.344

  }

  public func kilometers(_ kilometers:Double){

    distanceStat = kilometers * 1000.0

  }


I change the resulting code using these functions in the rest of the model. I’ll do the same for the other stat types. The current code only requires changes to the stat types DistanceStat, PaceStat and TimeStat. Pace and time add one wrinkle in two and three parameter setters. The first parameter becomes the name of the setter, and the rest are parameters like this:

public func hours(_ hours:Int,minutes:Int,seconds:Int) {

    timeStat = Double(seconds + minutes * 60 + hours * 3600)

  }

I didn’t do it intially, but some uses of these functions in initializers will need to specify self to work properly.

public init(minutes:Int,seconds:Int){

    self.hours(0, minutes: minutes, seconds: seconds)

  }

Running the code as written I get the same results, and no problems.

The 44 Warnings 

There’s one last thing to address: those 44 warnings. Closing the folders they are in, I find they fall into the following categories:

Those warnings are why Apple wants me to change this code, and threw me out of the app store to do it. This is all old stuff that needs updating. Sometimes living in the Swift world, I forget the Objective-C is improving over time too. The deprecations for example are all the same issue: there’s no more UIActionSheets in iOS.  Many of the others are number conversions which changed in newer versions of iOS in Objective-C

I have choices here: I can fix this code and remove the errors, I can ignore the errors for now, or I can delete the code now. While the errors are annoying, they are not affecting the code. I go for the last one. I will delete all of the Objective-C code before shipping, replacing it with the new Swift code. It doesn’t need to be there, but I use it as documentation for the old version until I don’t need it anymore.

So I got the new model into the old code. I started replacing components, and got the old code to actually function in the latest Xcode. My next steps are to get the views and controllers working properly. I”ll start that process not at the root, but by replacing those text views with UIPickers in the next installment.

I’m a LinkedIn Learning author, with five courses up in the LinkedInLearning library.  My latest is a trilogy for iOS notifications: Learning iOS NotificationsiOS App Development: Notifications, and watchOS App Development: Notifications If you a re interested in Notifications, I’d Check them out. 

This Old App: Models in the Playgrounds

Back a few months ago, an  app of mine  was removed from the App Store for being too old and not updated enough. In this series of articles I’ll document what I did on rebuilding and improving the app. In our first installment I analyzed the changes necessary. In this second installment,  I’ll start making the model.

I think in Model-View-Controller or MVC. If you are not familiar with MVC, I suggest you take a look at my video in LinkedInLearning on MVC and delegates. In short, a common programming practice is to break an application into three parts: the model, the View and the controller. The model is the code that processes data. The view is the user interface.  We don’t let the view and model interact with each other. Instead we use a controller to coordinate between the two.  This way we can change one while affecting the other two minimally.

While I might get some ideas for the view in my planning stages, I tend to start  coding with the model. In one sense the controller and view are window dressing for the model, where your data is. Views and controllers are the presentation and interaction with that model.

I’ll make a confession here: I hate working on the model. It the most tedious type of programming in my mind. It has none of the glamor or action of views or controllers.  It is just data, and I’m not much of a data person. As such I’ll give you a warning that this is a bit dry, but there’s a lot there to look at, and in the case of  RunCalc, some interesting problems that drove me up the wall the first time I wrote this code.

What’s in the Old Model

Experience in mobile applications has changed me, and the model in the original code was a reminder of this.  Written in Objective-C, The model for version 1.0 was a case of putting all the eggs in one basket.  I did everything in one model.  The original model had four properties to calculate running statistics:

  • Distance
  •  Time
  •  Pace
  • Speed

I added speed to handle treadmill settings, though it is measuring the same thing as pace: How fast the runner is going.

There are four core  methods which derive pace, speed, time and distance.  Basically you can derive each like this:

distance = pace * time

time = distance / pace

pace = time / distance

speed = distance / time

My measurements were seconds for time measurements, which I would format as strings in HH:MM:SS format for display. Distance was in miles. It was easy for me to calculate test cases in miles. I had conversion methods to metric measurements for miles and kilometers.  This meant that I had pace seconds per mile and speed as mile per second, which would have some more conversion to compute it to minutes per mile and miles per hour and kilometers per hour. Metric pace was in kilometers per seconds for some unknown reason that isn’t in my original notes. I think it was laziness.

This was the basics of the model, but there were a few more features that got added to it, The first and most important was the “locking” mechanism. There is a subtle problem with using the formulas above. Suppose you change pace. What do you update, distance or time? If you update both, then your numbers are wrong because things get updated on top of each other, and you can’t change all your settings. For changes in the calculator, what you need is to lock either distance or time in place and only compute one of them when you change pace. I did this with an enumeration in the early version to keep track of what I locked.

The last part of the model was running totals for the splits. It meant I had another set of four properties with totals for distance and time. There were two more for average pace and speed.

Building the new model : Properties and Units

The original had a lot of rounding errors and strange calculations due to switching between kilometers and Imperial measurements. Unlike other measurements systems, there will be a lot of switching between units, particularly in distance, and I really didn’t think this out well the first time. Long distance running uses different distance measurements at the drop of a hat. There are 5K, 8K, 10K, and 15k races. But there are half marathons at 13.1 Miles and full marathons at 26.2 miles. This and other measurements gave me the following measurements:

  • Distance
    • Meters
    • Kilometers
    • Miles
  • Time
    • Seconds
    • Minutes:Seconds
    • Hours:Minutes:Seconds
  • Pace/Speed
    • Meters per second
    • Minutes per mile
    • Minutes per Kilometer
    • Kilometers per hour
    • Miles per hour

Instead of computing in the units I have displayed, I kept with two standard units, meters and seconds, and converted everything else from them. These units match units found in other API’s such as CoreMotion, if I ever decide to interface this with other apps or API’s.

Pace and speed are different ways of describing the same thing, so I made them the same with one measure: Meters per Second.  Everything else  is a conversion.

Derivation Functions and Locking

The derivation function problem the first time took me a month to come up with the enumeration solution. For some reason I could nt wrap my head around the initial problem of handling that many variables.  Version 2.0 will use a different solution the model. There are six methods that change the model, two for each of the three properties. Each one changed one of the two other properties For example for distance:

func distance(changedBy pace:Double) {
    self.distance = distance(pace: pace, time:self.time)
}
func distance(changedBy time:Double) {
    let newDistance = distance(pace: self.pace, time:time )
    self.distance = newDistance
}

The Model and Conversions

I built the model as described into two models. One, RunStats for the model described above and one RunStatsIntervals for a collection of RunsStats objects and calculations for finding summations of the collection data. It was here I ran into my problem: The summations would need their own conversions, which were a repeat of the conversions above.

Here’s where object-oriented programming gets its biggest strength: I made up three classes PaceStat, TimeStat, and DistanceStat, that had the conversions. IN stead of using Doubles in my RunStats class I used these three classes. I went one step more and made constructors that would work in any unit I wanted, making the result the correct unit.  For example here’s distance:

public class DistanceStat{
// Basic unit is meters
// you can use meters, kilometers or miles for this stat
private var distanceStat:Double = 0.0 //private variable so you must use the setters and getters.
//Constructors
public init (){
    self.distanceStat = 0.0
}

public init(meters:Double){
   self.distanceStat = meters
}

public init(kilometers:Double){
    distance(kilometers:Double)
}

public init(miles:Double){
    distance(miles:miles)
}

// getters distance
public func miles()->Double{
    return distanceStat / 1609.34 
}

public func meters()->Double{
    return distanceStat
}

public func kilometers()->Double{
    return distanceStat / 1000.0
}

//Setters
public func distance(meters:Double){
    distanceStat = meters
}

public func distance(miles:Double){
    distanceStat = miles * 1609.34
}

public func distance(kilometers:Double){
    distanceStat = kilometers * 1000.0
}

}

Now to specify a 8K distance, I can say

let newDistance = DistanceStat(kilometers:8.0)

and return that as miles.

print( newDistance.miles())

I do the same with pace and time. With the pace, time, and distance stat in their own classes, I can use them anywhere and get the conversions I need anywhere.  With that I can make summation functions.  I’ll total the distance and the time, and use them to get an average pace for my summation data. That summation data can be in any of the units I have for that type.

I built the model not in the app, but in a separate playground. Playgrounds, especially for models offer an excellent way of looking at all your code and making the debugging process easier.  I can type them in on either my Mac or my iPad and check how every part works, finding errors as I go.  I did for example find an error in my  conversion formulas, and was able to find and fix it from the playground.

In the next installment we’ll add the model to the old Xcode project, get more than a few errors and start hooking up some user interfaces for testing.

This Old App Episode 1: The Evaluation Stage

Before anyone thought that home improvement could be sexy, WGBH Boston started a series on house restoration. This Old House is  still going strong after 36 seasons. If you’ve never watched, each season a house in bad shape undergoes restoration. The cast goes through the paces of find what’s wrong, finding solutions and doing the work to make a new, beautiful home.

Recently, I headed over to my often neglected iTunes connect account, logged in and was met with the bright and friendly message:

 The following apps have unresolved issues and have been removed from the App Store:

Interval RunCalc

Going into the app, I found the message I expected to see:

Dear Developer,

On September 1, 2016, we announced that we’re implementing an ongoing process of evaluating and removing apps that no longer function as intended, don’t follow current review guidelines, or are outdated.

We noticed that your app has not been updated in a significant amount of time.

Next Steps

To keep your app on the App Store, submit an updated version for review and make sure it follows the latest App Review Guidelines. If you are unable to submit an update within 30 days, your app will be removed from the App Store until you submit an update and it is approved.

If Your App is Removed

Your app will remain fully functional for current users. They will experience no interruption to services, will still be able to buy in-app purchases, and can re-download the app while restoring from an iCloud or iTunes backup. However, we recommend that you update your app as soon as possible to reinstate it on the App Store and ensure that it remains functional and engaging for new and existing customers.

You can continue to use your current app name, as your app has not been deleted from your account.

Yep my app was one of the one removed in the great culling Apple threatened back in the fall of 2016. IntervalRunCalc was one of the apps that hit the chopping block. I wasn’t upset since the app had  only 13 sales in 2016. This isn’t big money, so really its no big deal if it lives or dies. If it had been more popular,  I would have been doing the revisions necessary to keep the app

I’ve just finished writing a piece for LinkedIn on Peanut butter and programming, which was a metaphor on what developers go through on a regular basis. I thought to follow up on that metaphor, I ‘d go through the process with a real app. I could go through the whole development process of Interval RunCalc, rebuilding the app using Swift instead Objective-C. Unlike my usual API of the week column, I thought of a This Old House type series on rebuilding an old app.   So for the next couple of weeks, I’ll be doing just that. We’ll explore the old application, then build models, new interfaces and putting it all together in controllers. We’ll go through the debugging process for the new app, and my adventure in getting the new app processed for Apple.  Along the way I’ll go through some good habits, find some old bad habits, and let you see my decisions as I rebuild the app.

The Original Interval RunCalc

Before we begin, you need to know the story of the app. Knowing the story of your app sets a lot of your design decisions.  You’ll need to know a little bit about running and one particular running coach, Jeff Galloway.  Running is a very popular form of physical exercise around the world. There is a lot of concern among non-runners and beginner runners about injury. Jeff Galloway, a 1972 Olympics  10,000 meter competitor for the U.S. and later running coach formulated a simple solution to the injury problem: don’t run so much by walking a little. His Run/Walk/Run method adds walk breaks or intervals during a run. You could for example run one minute, recover for thirty seconds, and the run another minute. You can workout on more complex patterns.  My current training runs for the upcoming race season is run four sets of  30 seconds run, 30 seconds walk, then pause for an full minute walk break, then repeat those for the length of the run.

For beginner runners, this has made entry into running easier, and has probably been one of the contributing factors to the popularity of race running. For older runners, the impact on bones is far less. There is a lot of evidence for cardio benefits of alternating between high intensity to low intensity activities repeatedly. Some faster distance runners are beginning to experiment with 30-second and less walk breaks once a mile to combat fatigue and run faster late in the race.

If you want to plan out a run walk run interval pattern to achieve a specific time or pace, there was no calculator to do it.  As a runner, I originally did it on spreadsheets, but the measurements systems necessary for pace, time and speed made such efforts clumsy. I wanted a tool, and figured others did too that did the calculations for me quickly. Input some numbers for intervals and walk breaks. Get back a pace, time and distance traveled. That’s why I wrote version 1.0 of Interval RunCalc.

The home screen contained basic information on pace, distance, time and speed. By changing one of these four variables you could see how it affected the other four.

This interface was very confusing, since you need to press the correct button and enter the figures correctly. Even for me, the developer, this was confusing. I fell victim to a very easy trap many developers fall into: Not testing the UI and believing what works is good enough.

Unless you know better, there were two more views in the app.  They were hidden in a button at the top.

 

Tap the button and you got a menu:

The Splits Calculator gave the splits at mile intervals and calculated your time if you decided to run some of those miles slower or faster.

  The interval calculator was the reason for the app. The user would input a series of intervals for the Pace,Speed,distance,and time.The calculator would give average pace, distance, and elapsed time back. The idea was to plan interval training runs using the app. Again, these interfaces worked, but was not easy to use.  I also added a few buttons at the bottom of the app that were confusing for adding and duplicating the intervals.

The last series of scenes I made were input scenes. Users would enter in values for HH:MM:SS time or MM:SS pace. A decimal keypad would enter  distance and speed. These all presented huge data validation problems, particularly the time.

    

Enter the Watch

A few months after the launch of Interval runcalc the Apple Watch came out. As fitness watch, it worked well with Apple’s products but not other companies. As of this writing, many still have annoying latency problems. You might  look at your watch for several seconds before getting an accurate number for your pace, time and distance. When I moved to an Apple watch from my Nike+  GPS watch, this drove me nuts. There is one exception to this latency problem: Apple’s native fitness app, which also has the active heart rate monitor data. Apple’s watch app however has no interval notifications. You can find those on a few of the big run-tracking apps, but not apple’s fitness activity app.

One solution did present itself. One of the 3rd party apps I used did intervals and sent a local notification to the phone when it was time to change intervals. When the phone went to sleep two minutes into a run, the notifications went to the watch — I had my notification by a tap on my wrist. I’d run both the app and the the apple fitness app at the same time.

If we’re going to update runcalc, there’s this one active thing it could do: send local notifications at the run/walk/run intervals. That way you can run this app in the background while using an apple watch to do the run.

Other Possible Changes

The original app was for iPhone only, I’d like to get it working well on a iPad, and iPhone plus in portrait might need a different look. These are both the regular size width class, and thus the issues would be related.

There’s one more feature I might want that isn’t there. Most of the running apps that allow additions of intervals for your workout want to know how many repetitions of the interval you want. For a one minute walk and one minute run, this is relatively easy. But for a 30 second run and 16 second walk, it isn’t so easy.  Knowing how many repetitions of this interval set make up a workout would be a helpful number.

Evaluating the Changes

Given the app’s current condition, let’s summarize the problems and ideas so far:

  1. The initial scene is confusing to use
  2. The two secondary scenes, splits and intervals are hidden
  3. Intervals are not easy to read
  4. I never use the splits
  5. The intervals are hard to use
  6. Input of data is difficult
  7. Add notifications for intervals
  8. Repetition calculation
  9. iPad use(regular width size classes)

I’ll find more as I go on, but this will give me my starting point, and set me planning.

There’s a bit of advice I’d like to start with before evaluating these. When writing an app, keep to your story, the reason your app does what it does. Consistent stories make stronger products. The story of Interval RunCalc is a calculator for running interval calculations. I’m working with the number for planning purposes, not running with this. With that story firmly in my mind, I start to make decisions.

The story is about calculations for intervals, yet I set intervals on a secondary page, not the main page. It needs to be up front. Looking at the first page , I have a lot of extra whitespace. What if I added the intervals in that whitespace?

Here’s another thought: a simple continuous run is a single interval. As long as I always have one interval in the table the data on top summarizes all runs. I’m now doing two pages on a single page.

I’ll start wire framing, sketching what this might look like:

I used the simplest, fastest way to wireframe. In this stage, don’t get overwhelmed by apps and templates letting you lay out a wireframe app. Paper,  pencil, and a few highlighters works fine. As I did here I used the sketching mode of the Apple Notes app. I can clean this all up later. Work fast, or you’ll get so bogged down in distractions you’ll never get anywhere.  I’ll add two buttons on the bottom for adding an interval and clearing the calculator:

Then I get into a debate with myself: Should there be a third button, a totals button? If I select an interval on the lower half to edit it, Should I edit it in the upper Half? If so, I’ll need a button to deselect all intervals and set the upper half to totals? While compact, It again presents me with a problem: A hidden or confusing interface. It would be better to select a row in the intervals, then go to a second view to edit and enter there.

 

This would look nice and consistent on the home screen too.Each might be a button, and tapping it could change it.  I’ll also add a button for the repetitions as well.

In a few user interface changes, I cover most of the issues with 1-5.

What to Cut

My first cut is notifications. That should be a separate app.  I do have data that would make the notifications easy to add, but that does not mean I add it. Notifications are not in the story.  This happens a lot in development, often referred to as feature creep.  One keeps adding functions to an app that it becomes, slow, unwieldy and overall unusuable. One classic example is Microsoft Word, which has so many extra features outside its core story of typing out a document. I’d rather use a simpler general word processor like Pages or Google Docs instead of Word for most documents. In updating an app, unless its a feature demanded by users, avoid adding anything outside the story. If you are an independent developer, this is a lot easier than being in an organization however. You might have teammates, upper management or marketing telling you to put something in that makes no sense. The advantage of getting the entire organization to  buy into the app story is keeping everyone focused.

For the same reasons, I’m dropping the splits calculator.  Since this is an Interval Run calculator, Splits are  not in the story.

Input Views

I’ll make one more change that I have been thinking about. I didn’t like the input in V1.0.  My original notes for 1.0 tell me I hated the time it takes to use a UIPickerview as a user.  In V2.0 I’ll change all my input to picker views to restrict the user, which they are very good at doing. Unlike other picker views I’ve seen, I’ll speed up the picker with more components. Instead of 60 rows in the seconds of minutes picker component, ill have components of 0 to 5 in one and 0 to 9 in the other, so I can quick dial any number.  My time pickers would look like this:

This solves a lot of the validation problems I had with the text fields by restricting input, but speeding up the input.

The Regular Width Devices

Our final problem in our list is the regualr width devices of iPad and iPhone plus in landscape. I have a simple solution to that. I’ll have two major views, the intervals and the stats. In regular width mode, they will be next to each other instead of on top of each other

With all that decided. I have the beginnings of a new version of this app. Many of the user interface decisions were made and those influence the structure. I was a good boy and for the moment I added only one more new feature, and killed several features. I have a few more user interface decisions to make, but I’ll get to those a little later.  My next step is to get into the code, and I usually start that with the models I’ll be using in the app. In the next lesson, we’ll build those new models, and look at the old code to find some of the problems I encountered with the old model I might want to change in the new one.

Short Repeating Notifications in iOS

In an earlier post, I discussed the repeating local notifications in the UserNotification framework, but that discussion misses one detail: repeating notifications less than 60 seconds. The framework will very stubbornly refuse to have a repeating notification less than a minute, giving a runtime error if you do.

In my LinkedInLearning course Learning iOS Notifications, I mentioned a workaround to get around this problem, but didn’t show how to in that course. Today, I’ll show you how to get repeating notifications for times less than 30 seconds.2017-02-27_06-16-49

Before I do, I will give a big warning from the user experience perspective. Rapid-fire notifications are annoying to the user under most circumstances. Don’t do this unless the functionality of the application is such that the user needs the frequent interruptions. Almost all the cases I can think of  are interval timers.

In fitness,  interval training is a recommended training regimen with lots of benefits .  There’s even a movement  created by U.S. Olympian Jeff Galloway among distance runners  to do run/walk/run a interval-based distanced running strategy to reduce injury. There’s also speedwork intervals for runners. It’s here that you’ll often see those less than 60 second time intervals repeated. Often it will be 30 seconds of intense activity repeated with lesser activity for a rest period. In this lesson, We’ll set up a 30 second repeat timer to show you the workaround to the 60 second limitation.

I’m going to assume you have a good idea of how to use local notifications for this lesson. If you don’t, try this lesson first or even better take my video course Learning iOS Notifications on Lynda.com or LinkedInlearning.

Make a New Project

I’ve made a starting file for this lesson which you can download here: shortrepeat_start If making this yourself, Make a new single view project ShortRepeat.  When the project loads change the display name to Repeat 30s2017-02-24_05-47-53

Go to the storyboard. Drag out two buttons and a label. Title and arrange them like this:

2017-02-24_05-53-15

You can leave this like this for this lesson if you want to code. I’ll clean them up in a little auto layout and color:

To see how I did this, you can download the start file. 2017-02-24_06-06-31

Open the assistant editor and add two actions to the code:

@IBAction func startButton(sender:UIButton) {
}
@IBAction func stopButton(sender:UIButton) {
}

Drag from the startButton‘s circle to the to the Start button. Drag from the stopButton‘s  circle to the Stop button.

You’re ready to code. You can download a starter version of this here: shortrepeat_start

Set Up a Notification

There’s a few steps we’ll need to set up before using a notification. Close the assistant editor and go to the ViewController class code. You’ll need to include the UserNotification framework first. Under import UIKit, add this

import UserNotifications

You’ll be using the user notification delegate too. Add its protocol to the class

class ViewController: UIViewController,UNUserNotificationCenterDelegate {

At the bottom of the class, add this to make the two delegates:

//MARK: - User Notification Center Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
    }

The usernotificationCenter:willPresent notification method assures us that the notification will show up and sound the alert when we are viewing the application. We’ll use the usernotificationCenter:didRecieve response: a little later to code a stop action, but for now I added the required completion handler.

Go to ViewDidload to point the delegate to the delegate methods.

UNUserNotificationCenter.current().delegate = self

Notifications require user permission, so add this to viewDidLoad as well:

UNUserNotificationCenter.current().requestAuthorization(
    options: [.alert,.sound]) 
    {(granted, error) in
         self.isGrantedAccess = granted
    }

You’ll set the notification for alert and sound, and set a variable isGrantedAccess as a flag to decide if you have been granted access for notifications.

Of course you haven’t defined isGrantedAccess yet. Add it to the top of the class.

var isGrantedAccess = false

You’ll also need a category with a Stop button action for the notification. Add this to viewDidLoad

let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: [])
        let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([timerCategory])
        

This adds a stop action, which will display a stop button on the notification.

Making a Notification

We’ve done much of the setup, it’s time to make the notification. The secret to the notification is to make a time interval trigger of near zero, to launch an immediate notification. You’ll use a timer to send the notification.

Make a new function sendNotification. Add an if clause to check if we are granted access

func sendNotification(){
    if isGrantedAccess{
    }
}

In the if clause’s block, add some content:

let content = UNMutableNotificationContent()
content.title = "HIIT Timer"
content.body = "30 Seconds Elapsed"
content.sound = UNNotificationSound.default()
content.categoryIdentifier = "timer.category"

This will print a simple message and will play the default sound when 30 seconds is up. It also will use the timer.category to display buttons.

Next add the trigger. The strategy is to use a trigger at zero, but a timeInterval of zero is not allowed either. Use a time interval trigger set to a small interval such as 0.001, with no repeat to send one notification immediately.

let trigger = UNTimeIntervalNotificationTrigger(
    timeInterval: 0.001, repeats: false) //close to immediate as we can get. 

Finally make and add the notification request to the current user notification center. For error handling, I just print a message to the console.

        let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error{
                print("Error posting notification:\(error.localizedDescription)")
            }
        }

To test out this function, add it to the startButton method

@IBAction func startButton(_ sender: UIButton) {
        sendNotification()
    }

Build and Run. you’ll get the permissions alert:

2017-02-24_07-30-38

Tap Allow.  Hit the Start button and you immediately get a notification

2017-02-24_07-31-57

Repeat the Notification

I’ve gotten the notification to fire pretty close to immediately. Now I’ll add a timer to fire repeatedly every 10 seconds. Add to the top of the view controller class a timer

private var timer = Timer()

Code the following function to start a timer:

func startTimer(){
        let timeInterval = 10.0
        if isGrantedAccess && !timer.isValid { //allowed notification and timer off
            timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in
                self.sendNotification()
            })
        }
    }

For this code, I used the Timer initializer with a closure named block, instead of the one with a target. We have only one line of code to send the notification, to execute every time the timer completes an interval, so using a closure is more compact and easier to read than calling another function. I placed the timer’s start in a if clause to prevent the timer from starting if we don’t have permission for notifications and if the timer is already started. We don’t want to re-start the timer once started because that will mess up the intervals we are timing, and it is possible for someone working out to accidentally hit the Start button on their phone. I also set the time interval to a shorter ten seconds for testing purposes. I’ll move it back to 30  as shown in the notification when the app finishes testing.

Stopping the Notification

I’ll need to stop the notifications firing when I’m done with my workout. To do that, I’ll make another function to invalidate the timer. I’ll also clean out all the notifications

func stopTimer(){
    //shut down timer
    timer.invalidate()
    //clear out any pending and delivered notifications
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
} 

There’s nothing useful about having a timer notifications floating around, so getting rid of them when you stop the timer makes things nice and tidy. There can be at most two here, because all pending notifications replace each other. Therefore you may have one pending notification, and very likely will have one delivered notification at most.

I’ll place the stopTimer method in two places. First, I’ll place it in the stopButton

@IBAction func stopButton(_ sender: UIButton) {
        stopTimer()
    }

Secondly, I’ll add it as the action for the stop.action on the notification. Drop down to the user notification center delegates at the bottom of your code and change the userNotificationCenter: didReceive response: to this:

func userNotificationCenter(_ center: UNUserNotificationCenter, 
    didReceive response: UNNotificationResponse, 
    withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.actionIdentifier == "stop.action"{
            stopTimer()
        }
        completionHandler()
    }

Set your simulator device to an iPhone6. Phones later than this one use 3D touch in the simulator and you may not be able to get the action to appear otherwise with mice and older trackpads in the simulator. Build and run.

Unless you were using an iPhone 6 simulator earlier in this tutorial, the phone should boot and then ask permission again.

2017-02-24_09-34-15

Select Allow, and then Start. Notifications will start to appear every ten seconds.

2017-02-24_09-24-54

Press Stop The notifications stop. Press Start again and then Command-L to lock the phone. You’ll start to see notifications on the lock screen.

2017-02-24_09-35-36

Swipe toward the left and you’ll see two buttons

2017-02-24_09-35-54

Tap the View button.  You’ll see your notification and the action button. Tap Stop and the notifications stop.

2017-02-24_09-36-11

Bonus:The Apple Watch and Repeat Notifications

The Stop action button is something you should definitely put into your application. On a long run, it takes a while to unlock the phone and hit Stop in the app.  Opening the notification is faster. There is the other place this notification might show up: An Apple Watch

2017-02-24_09-49-41

Local phone notifications appear on an Apple watch paired to a phone that is locked or asleep. There may be a slight time delay, but they will show up. On a workout where you start your app, the phone will eventually lock during the workout.  The paired Apple watch will continue to get the notifications. Since we included sound as part of the content, that will mean the watch will include a haptic touch for the notification.

This method of repeat notification does have it limitations. It does run in the foreground and does use more processor time and power than the repeat notifications. You’ll need to set it up for background timing if you turn the app off or the device times out. If you do not have to do time intervals less than  60 seconds use the framework instead of this.

The Whole Code

You will find the complete project here: shortrepeat_end It includes some extra watchOS files and some graphics  to make the watch and project look a little nicer.

//
//  ViewController.swift
//  ShortRepeat
//
//  Created by Steven Lipton on 2/24/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

class ViewController: UIViewController,UNUserNotificationCenterDelegate {
    var isGrantedAccess = false
    private var timer = Timer()
    
    func startTimer(){
        let timeInterval = 30.0
        if isGrantedAccess && !timer.isValid { //allowed notification and timer off
            timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in
                self.sendNotification()
            })
        }
    }
    
    func stopTimer(){
        //shut down timer
        timer.invalidate()
        //clear out any pending and delivered notifications
        UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
        UNUserNotificationCenter.current().removeAllDeliveredNotifications()
    }
    
    func sendNotification(){
        if isGrantedAccess{
            let content = UNMutableNotificationContent()
            content.title = "HIIT Timer"
            content.body = "30 Seconds Elapsed"
            content.sound = UNNotificationSound.default()
            content.categoryIdentifier = "timer.category"
        
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.001, repeats: false)
            let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger)
            UNUserNotificationCenter.current().add(request) { (error) in
                if let error = error{
                    print("Error posting notification:\(error.localizedDescription)")
                }
            }
        }
    }
    
    
    @IBAction func startButton(_ sender: UIButton) {
        startTimer()
    }
    @IBAction func stopButton(_ sender: UIButton) {
        stopTimer()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound]) { (granted, error) in
                self.isGrantedAccess = granted
        }
        let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: [])
        let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([timerCategory])
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
//MARK: - User Notification Center Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.actionIdentifier == "stop.action"{
            stopTimer()
        }
        completionHandler()
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
    }

}