This Old App: There is Always One More Bug

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. Last time, I got the app working. It calculated intervals and used those for bigger measurements. I loaded the app on my phone for a weekend of trying out the app. This installment I’ll discuss what happened when I did user testing. I don’t think its too much of a spoiler: I found bugs. Probably the most important principle of programming is There is always one more bug. Knowing what to do with them is the challenge.

The Bugs

Over a weekend where I had a 10 mile long run, I used Interval Runcalc several times to figure out some interval strategies. It worked even better than I ever imagined. I found a workflow there that I hadn’t anticipated, but the app does exactly what I wanted it to do. On the other hand, there was some wierdness. Let’s go to an example problem. Suppose I’m running a 5K training run using run/walk/run. I’d like a 30 second run of 11:30 min/mi and 40 second walk of 16:30 min/mi. What is my estimated finish time?

With this example I find the following bugs:

  • Setting the first interval, I set the pace to 11:30, and the time 30 seconds. The font and background are both too light and small. It’s hard to read the picker on a phone. They need more contrast and a larger font without so much white space.
  • The picker is also too close to the edge, so its hard to dial up the end numbers on a phone like mine which has a protective phone case on it.
  • I set a pace of 16:30, but the display reads 16:29. I try again for 16:31, which works and 17:30 which also works.
  • The time is still dimmed on both intervals. I need to make the unlocked time darker.
  • The calculator reads a pace of 13:54 on the top. I tap the distance on the top and change it to kilometers. The 0.04 miles of the 11:30 pace interval changes to 0.7 along with the distance on top tp 0.13km. I don’t want that behavior either, though I can change the 0.13 to 5 now. When I do, intervals clear, but calculates my time to be 43 minutes 13 seconds.

There one more thing I didn’t like. The display in landscape is not easy to use or read. It is too cramped. I’ll eventually fix that too, but I’ll defer that for a later installment, since that solution has a lot to do with layouts for the iPad.

In our previous user interface installment I mentioned I’d come back to the user interface. Only in real use do you find many of your user interface problems.

The Fixes

The easiest fix is the width of the picker. The picker’s width is set by a width constraint, which I tighten up by making 75% of the width of the screen by changing the multiplier to 0.75.

I’m not happy anywhere about the colors on the picker. I don’t like the color changes. I’m going to standardize the colors for all uses of the picker. In the storyboard, I change the colors on the picker. Using the colors from the root view, I make a blue toolbar and green picker. I’ll comment all places where I invoke the colors method of my picker

//vc.colors(view: view, button: sender)

In the pickerViewController, I comment out the title font settings and replace with a system font of 36 point.

//label.font = UIFont.preferredFont(forTextStyle: .title1)
label.font = UIFont.systemFont(ofSize: 36.0)

Which looks a lot better in portrait

But has problems in landscape. I’ll come back to landscape later.

The 16:29 Bug

The next problem I’ll address is the 16:29 bug. I suspect it is a rounding error. On both the intervals and the root, if I input 16:30 minutes per mile, I get 16:29. If I’m right, the bug will be in this code

public func minutesSecondsPerMile() -> String{
    let minutes = Int(secondsPerMile()) / 60
    let seconds = Int(secondsPerMile()) % 60
    return String(format:"%02i:%02i",minutes,seconds)
}

I’ll add a print statement here to see what’s going on by looking at the remainder as a double.

print("seconds per mile:\(secondsPerMile()) \(secondsPerMile().truncatingRemainder(dividingBy: 60.0))")

For the 10:00 min/mile default, this prints:
seconds per mile:600.0 Remainder:0.0
Change the pace to 16:30, and this prints:
seconds per mile:990.0 Remainder:29.9999999999999
As suspected it’s a rounding error in Swift. The remainder should be 30. For 990 seconds and 999 (16:39) seconds I get the rounding error on the remainder. With an old school rounding, I can remove the problem.

let minutes = Int(secondsPerMile() + 0.5) / 60
let seconds = Int(secondsPerMile() + 0.5) % 60

I’ll do the same for picker, which is missing the rounding.

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

Trying this out, It works. I’m getting 16:30 as 16:30 and 16:39 as 16:39. I tried simulating this in a playground and it didn’t find the same errors. This isn’t something I did but a quirk in Swift when running an app, so I’ll just adjust to it, and avoid 999 seconds and 990 seconds,

Dimmed Intervals

Next to fix is the dimmed intervals. I did that when updating the table cells. I checked the locked state of the interval in the cell and set the display accordingly. Since I’ll be messing with that interval more than I did originally, I changed my original code to this, creating an object intervalCell to hold the stats for this cell.

let row = indexPath.row
let intervalCell = statsIntervals.intervals[row]
cell.runStats = intervalCell

I’ll use intervalCell’s locked property to determine what to dim and what to brighten.

 if intervalCell.locked == .distance{
      cell.distanceEntry.setTitleColor(lockedColor, for: .normal)
      cell.timeEntry.setTitleColor(baseColor, for: .normal)
    } else {
      cell.distanceEntry.setTitleColor(baseColor, for: .normal)
      cell.timeEntry.setTitleColor(lockedColor, for: .normal)
    }
    cell.paceEntry.setTitleColor(baseColor, for: .normal)

I added two color constants to make this easier to write:

let baseColor = UIColor(red: 0.1, green: 0.5, blue: 1.0, alpha: 1.0)

let lockedColor = UIColor(red: 0.5, green: 0.7, blue: 1.0, alpha: 1.0)

I’ll add a distance and a time interval, and this time I get the stats I’d like to see.

A New Bug

A new problem creeps up on me. A friend set a new 5K personal record, running 6.2 miles an hour. I’m more used to pace and time to judge 5K’s so I plug it into the calculator. I set the distance to 5K, and change pace to speed. The display shows 6.0 mi/ hr.

I tap the 6.00, and I get a suprise.

If I tap done I get 600 mph.

tap the 600.00 and I get 6.00

The value is backwards in mph somewhere. My first suspicion is the picker, since the components might be backwards. However that uses the same methods as the distance, which is working fine. Being the old school debugger I am, I add a print statement to the setTwoDecimal method of the PicekViewController

print("Value in two decimal \(value)”)

I see what I get. For the distance 26.2 and 13.1 I get exactly what I expect. For the miles per hour, I get that 600.
Value in two decimal 26.2
Value in two decimal 13.1
Value in two decimal 600.0
My next suspect is the model’s conversion method for miles per hour. I know the value is stored right as meters per second, since pace works without a problem, so the system converts from miles per minute to meters per second without problems. A print statement in the milesPerhour gives the usable value of 6.0000099456. So my next suspect is calling the picker.

I set up another print statement in the case so I have exactly what I sent to the picker:

case .milesPerHour:
  vc.units = .milesPerHour
  vc.value = runStats.pace.milesPerHour()
  print ("Sending Units \(vc.units) Value \(vc.value) to picker")

That gives me something interesting to look at
Sending Units milesPerHour Value 6.0000099456 to picker
Value in two decimal 600.0
I try the picker again. The first time this value gets used is in the configure method. Another print statement and I get this:
Sending Units milesPerHour Value 6.0000099456 to picker
Configure for two decimal 600.0
Value in two decimal 600.0
It happens between setting the value in the case and presenting it. And there it is, right before the present method:

vc.value = runStats.pace.secondsPerMile()

That was a test statement I hadn’t marked correctly as a test, and thus forgot to delete or comment out, interfering with the correct value. Commented out, I run again. The print statements now read correctly:
Sending Units milesPerHour Value 6.0000099456 to picker
Configure for two decimal 6.0000099456
Value in two decimal 6.0000099456
So I’ll try the 5k at 6.02mph

She ran a 30:58 5K. I’ll change the speed back to pace:

That would be my dream pace. I’m very impressed with her and with runcalc for working again.

More Table View Cell Bugs

The last bug brings up the last installment again. Let’s say I set up the intervals like this:

Then change to kilometers for the distance.

Both the first interval and the distance changes to kilometers. I only want any new intervals to change to kilometers, not current ones.

I know where the problem starts. At the core of this problem is the currentRowproperty and method. Before I get started on that, I remove all the print statements from my last bug, then start do some exploring with a few more prints.

I’ll look at the logic of this one first. This all appears to happen in IntervalTableViewcontroller. There is a property currentRow

 var currentRow = 0

There is also a method currentRow

func currentRow(button:UIButton){
   let cell = button.superview?.superview as! IntervalTableViewCell
   guard let indexPath = tableView.indexPath(for: cell) \
   else{
     print("not a cell")
     return
   }
   currentRow = indexPath.row
}

The property currentRow is an index of the active cell’s interval stats. If you remember back to an earlier installment, I use this for updating the correct interval based on the button I press. For example in distanceEntry’s action, I get the correct row to change like this:

@IBAction func distanceEntry(_ sender: UIButton) 
    currentRow(button: sender)
    let runStats = statsIntervals.intervals[currentRow]
…
}

I also use it in updateDisplay to point to which line I’m supposed to recalculate.

  func updateDisplay(){
    let runStats = statsIntervals.intervals[currentRow]
    runStats.recalculate()
    tableView.reloadData()
    delegate.didChangeStats(stat: runStats, controller: self)

  }

The offending behavior happens when we update the units. So I expand my search to the RootViewController’s updateDisplay. The very last line updates the table.

//update the table

    tableViewController.tableView.reloadData()

I’m looking at this line and I’m wondering something: Why do I update this here? I go back in my docs and this was the big error from the last installment. It is still haunting me. I need better thinking out of the problem.

First of all let me be clear of what I want: When the units change on top, the template for the next added interval changes. All the current intervals remain the same. That’s what I want.

When I hit an action to change a unit, I call a method in RootViewControllerupdateTableUnits.

func updateTableUnits(){

    let template = tableViewController.runStatsTemplate

    let paceUnits = runStats.pace.displayUnits

    let distanceUnits = runStats.distance.displayUnits

    template.pace.displayUnits = paceUnits

    template.distance.displayUnits = distanceUnits

  }

That changes the units on the template — or it is supposed to. I put a property observer on this, and I get no results. I make a small change to this.

tableViewController.runStatsTemplate = template

The template is updating according to my print statements. I think this all has to do something with the current row, so I’ll add a property observer with print to it

var currentRow = 0{

    didSet{

      print("currentRow changed to: \(currentRow)")

    }

  }

I run again. I change the units for distance and get no response from the current row. I add another row and get no response from the current row. I change the first interval to 10:00 and get a

currentRow changed to: 0

On the consle. I’m wrong. It’s not current row that’s the problem. Changing stats are not changing current row, just row 0. I search places where row 0 occur, and don’t find anything that matters, but do find some junk to clean. I had an array of intervals in root, and I’m not going to use it, so I delete it.

This is the point where I’m spinning my wheels on ice. I’ve reached analysis paralysis and don’t know which way to go next. So I do one thing that might be the easiest way to look at the problem: I break it more.

//self.updateTableUnits()

I comment out the update of units of the table. When I do that, there is no update of the cell. That tells me that its somewhere in that method.

That method I changed in my messing around. I made it more modular by replacing this code

let template = tableViewController.runStatsTemplate

    let paceUnits = runStats.pace.displayUnits
    let distanceUnits = runStats.distance.displayUnits
    template.pace.displayUnits = paceUnits
    template.distance.displayUnits = distanceUnits
    tableViewController.runStatsTemplate = template

with this code

tableViewController.changeTemplate(
      paceUnits: runStats.pace.displayUnits,
      distanceUnits: runStats.distance.displayUnits
    )

Which runs a new method on the table view controller.

  func changeTemplate(paceUnits:PaceStat.DisplayUnits, distanceUnits:DistanceStat.DisplayUnits){
    runStatsTemplate.pace.displayUnits = paceUnits
    runStatsTemplate.distance.displayUnits = distanceUnits
  }

Since all this does is set two properties for runStatsTemplate, the problem is in some use of run stats template. I try searching for all of them. I think it may be this one:

override func viewDidLoad() {
    super.viewDidLoad()
    statsIntervals.intervals[0] = runStatsTemplate
    // Do any additional setup after loading the view.
  }

I looked at this line before, but missed it for that one subtle error every newbie is cautioned against, yet still gets everyone at some point: runStatsTemplate is a pointer. If I set two specific properties like this:

override func viewDidLoad() {
    super.viewDidLoad()
    statsIntervals.intervals[0].distance.displayUnits = runStatsTemplate.distance.displayUnits
    statsIntervals.intervals[0].pace.displayUnits = runStatsTemplate.pace.displayUnits
  }

The application works the way I want it to with this change. Realistically, I don’t need or even want the runsStatsTemplate. I can replace everything for that with this struct:

struct DisplayUnits{
  var paceUnits:PaceStat.DisplayUnits = .minutesSecondsPerMile
  var distanceUnits:DistanceStat.DisplayUnits = .miles
}

I’ll refactor the code to use this instead of a full RunStats.

override func viewDidLoad() {
        super.viewDidLoad()
        statsIntervals.intervals[0].distance.displayUnits = displayUnitsTemplate.distanceUnits
        statsIntervals.intervals[0].pace.displayUnits = displayUnitsTemplate.paceUnits
        
    }

I’m going to do a few cleanup things now. I’ll backup this project completely, and then get rid of my old code, except the app delegate and the bridging header. At this point I really don’t need it anymore, and I can get rid of all those annoying warnings about out of date code. I’ll run this. And my app is pretty much working the way I want.

Bugs are part of development. They will happen, and more often than not, they will be silly mistakes, though their effect on the app may be major. Some are user interface problems, some functional problems. It takes a lot of work, usually more than development to get rid of bugs. This is only the first round of getting rid of bugs. As I titled this installment there is always one more bug. No application is bug free.

There’s two more bugs I know of. One bug I didn’t address yet is the bad user interface in landscape. I have an idea about that, and we’ll work with auto layout to make an iPad and iPhone landscape layout that do work in a future installment. Before that, I need to address some other issues. What happens when an indie developer shows their code to other developers?

Steve Lipton is the mind behind makeapppie.com for learning iOS app development. He is the author of several books on iOS programming and development. He is also a LinkedIn Learning and Lynda.com author. His latest courses are Advanced iOS ApplicationDevelopment:Core Motion and Learning Swift Playgrounds Application Development

One Reply to “This Old App: There is Always One More Bug”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s