Three Tries with NSTimer
I wanted to try the a new running program 30-20-10. My usual running app does a lot of things well but it's not great at measuring intervals in seconds instead of minutes.
That's when the programmer's curse struck. I wanted to try writing an iPhone app in Swift for a while and now I had the perfect excuse: I had a project.
All code referenced in this post is available on GitHub
First Try
The intervals were easy to model. Enums in Swift can have methods which is perfect for adding the alert sounds and text. Once I had the list of states it was easy to fill in the actions. Speed.swift
Scheduling the timer was easy once the intervals were modeled. We have a run loop of:
timer fires ->
do stuff ->
set next timer
More concretely:
That's all we need for beeps and text.
So I was done.
Except... that screen looks empty.
"Jog", "Run", and "Sprint" aren't very motivating by themselves and the iPhone has a lot of screen to fill so I thought perhaps the app would look more polished with a progress indicator.
The programmer's curse struck again! There are all kinds of circular progress bars but I couldn't seem to find one that matched the image in my head so I spent too much time writing one of my own. CircleProgress.swift
To be motivating, the progress indicator would need to update more often than the running intervals; a repeating timer on a half second interval does the trick.
First Try Problems
Two timers make pausing too problematic. As much as I may want to do all of my runs straight through, I'll probably need to take a break at some point. I needed the app to easily pause for things like phone calls or stopping for traffic.
Second Try
One repeating timer can update both the intervals and the progress bar. To keep things clean, I separated managing the timing of the intervals--- the RunFor portion of the enum--- from updating the progress bar.
I didn't have to update the progress indicator since that code already handled the half second updates.
The alarms were another matter. Instead of triggering when the interval fired, I had to update that code to keep track of how much time had passed. That was pretty easy after I added a class variable and this function:
Second Try Problems
The progress bar kept going over 100%. Frustratingly, there was also no consistency in how much over 100% it goes.
At first I thought it was a problem with the CircleProgress class, but I couldn't get it to fail again when removing the timer code. Running the app against a stopwatch, I quickly realized I'd been running for more than the 21 minutes I intended.
My program required a perfect timer. Timers never fire exactly at a set interval. They fire incredibly close to that interval. And, since my loop only checked to see if the interval had passed, it was always a little late. Never more than half a second, but with several intervals a minute those half seconds add up.
That makes sense now, but I wasn't thinking that way at the time.
Third Try
Rather than checking that each interval had passed, I modified the main loop to calculate the total amount of time that should have passed at the next interval. I was lucky in that the code wasn't really all that different.
Finally, I'm running for the correct amount of time.
The timer is still not perfect and will fire a fraction of a second late, but since the new timer doesn't start a fraction of a second late the program is kept on track.
With my drift problem solved, I could focus on fighting the hills by my apartment and not my program.
Next Steps
NSTimer only works when the app is in the foreground. I disabled the idle timer, but it would be nice if the app could run while a GPS program or another app was in the foreground.
Unfortunately, to preserve battery life, Apple really doesn't want app writers to keep their apps alive in the background. I think there are ways around that restriction but they're not really in the spirit of the rules so any one of them could cause an app to be rejected from the store.
But I'm not submitting this to the store quite yet so I'll be exploring some of those ideas in a future post. Stay tuned.