One year with RUN.
This is the story of how I came to develop my own running app. I'll dip into the weeds of iOS development, cloud servers, GPS nuances and user interface design. Ultimately, I hope to answer this question: can a sole developer, who is a novice runner, in his free time develop a running app that, for himself, is far better than any of the available running apps currently available?
TL;DR answer: yes, yes he can.
I was never much of a runner (don't worry, this isn't a full bio — just a couple quick paragraphs and then I'll move on, I promise). I skateboarded through my teens and 20s and spent my 30s getting comfortable and soft. The entirety of my running career was a very brief marathon training program in my early 20s (that I stuck with for almost a week) and participating in one organized 5k (which I mostly walked).
Several years ago though, when I lived in downtown Springfield, I took up jogging as a light hobby. I would mostly run 1-3 miles, maybe once a week. A couple years ago, I started running again when I was trying to lose weight. As I dropped pounds, I'd pick up the pace and add distance.
This brings us to last fall, when I was running more than ever. I had been using Runkeeper on my phone this whole time to log runs, and it was generally pretty decent. I felt like I only needed 5% of what the app was offering, and navigating around the bloat (and constant interruptions asking to join a challenge or upgrade to the paid tier) was tedious.
I briefly used Strava, Nike Run Club and some other apps. They all shared a similar feeling of having way more in the app than I cared about and too many clicks to get to what I wanted to see. And none had what I felt were some of the more simple visualizations that I felt would give me a great perspective of my overall progress. Maybe they were hidden behind a paid upgrade, but when an app already feels bloated, the last thing you want to do is throw money at it to layer on more bloat (as crickets chirp and silence hangs thick in the air, everyone in the room slowly turns to look at Adobe, who, sitting in department store jeans and an oversized Hoobastank t-shirt, sheepishly shrugs his shoulders).
One afternoon, while enjoying some free time at a coffee shop, I created the first version of my running app, which I named RUN. At that point it was a simple website designed to show my Personal Records and a glance at my overall performance.
I imported all my runs from Runkeeper, which lets you download a CSV of all run data and accompanying GPX files (thank you for this, Runkeeper!). I didn't even use the location files at first, just the raw data from each run. For new runs, I still used Runkeeper on my phone to record the run, then I'd manually enter the results on my web app after each run. It was a little tedious, but the payoff was worth it. I owned the data, and I could shape it into whatever I wanted.
The weird chart above is one of my favorite things about this whole endeavor, but without any labels, it's hard to tell what's going on. The graph represents time horizontally, with January at the left and December at the right. The black lines on the top half represent runs this year, and last year's runs are on the bottom. The longer the run, the taller the line. The red dotted line represents the current day. The number at the top of this line is how many miles I've run in the current year, and the number at the bottom of the line is how many miles I had run by the same day last year. The number at the bottom-right corner is the total miles from the pervious year.
More than anything else I've come up with on the site since this first version, this chart is the best way to quickly check progress compared to the previous year, to check how long it's been since my last long run, to check the variety of distances I've been running, and to check how much rest I'm getting.
The next thing I wanted to add was create a shareable image for obnoxiously posting runs on social media. Because, you know, if it's not on Facebook, did it even happen?
This was a bit of a puzzle for me, because I needed to programmatically create an image from the web server. This meant I had to use something other than the normal conventions of design using web fonts, HTML and CSS. I build my site with PHP, which has a couple built-in image libraries with their own pros and cons. I decided to use the GD image library, which I've used in the past, but never to this degree of complexity.
I had to figure out how to load in fonts, center text on an image (which, if you've ever had to get into the weeds of typesetting, you'll appreciate the complexity of this seemingly simple task), load in external images, draw translucent charts on the background, and lots more. It was a really fun challenge, and it all resulted in the ability of my site to have a "Share" button for each run that would deliver a single image that I could then share on social media. And I could do this all from my phone. Here's what the first sharable image looked like:
The images had a couple charts at the bottom that I know would make no sense to anyone else, but looking back, it would help me understand the run in context of where I was at at the time. The big yellow bar on the right represents the run that's being shared. The darker bars to its left are previous runs of the same distance. The height of the bar represents the pace, so shorter is faster. The skinny black lines at the bottom represent the top half of the chart on the first image above — all the runs for that year. The run that's being shared has a yellow line (very short in this case).
I continued using the web app for the next several months, and I found it to be a great source of motivation to keep running over the winter.
After having quite a few people ask about using the tool for themselves, I decided to open it to the public and let anyone sign up and enter their own stats. This required very little work on my end, as it was designed to be multi-user from the very beginning.
Despite multi-user support, I cautioned people before signing up. It was cumbersome to manually enter data from each run, and the metrics displayed were highly targeted at my own preferences. My weird and minimal site (including charts with no labels) made perfect sense to me. But how would anyone know what anything meant?
The iPhone app
I had gotten accustomed to manually entering data from Runkeeper into my web app after each run. But as time went on, I found myself wanting more control over how the runs were recorded on the iPhone. I wanted weather data, automatic route detection, audio alerts during a run telling me how fast the last mile was. Eventually it became clear that to really do what I wanted with this tool, I needed to stop using Runkeeper and make my own native iPhone app.
After development of my first iPhone app, Beer Dude, I felt comfortable programming in Swift for iOS. The basic app framework was simple to create. Like with the web app, I built in multi-user support from the start, and the scope of the native app was clear from the beginning, which is a huge help when developing something from scratch.
One problem I had to solve with this app that I didn't encounter with Beer Dude was how to store things in a database on the user's phone and sync it with a similar database that lives on a server (ie, "the cloud"). I explored some external libraries that were built to solve this problem, but none felt like a great solution. So in typical Dave fashion, I decided to build my own complete cloud-syncing solution.
Brief aside here: In programming, it's easy to catch the "Not Invented Here Syndrome," where you repeatedly reinvent the wheel instead of just using existing libraries that solve the problem. There's probably a name for the opposite syndrome, where you just cobble together miscellaneous libraries in order to kind of solve a problem.
There are many situations where using external libraries makes sense. A couple standbys for me are jQuery and Chart.js. Before writing my own, I always research what's out there, and sometimes I find a good fit. But I'm frequently reminded of my experience with Dreamweaver in the 90s (which was a program that tried to make website design easy, writing the HTML code for you). Dreamweaver isn't the best analogy for a software library, but neither is saying "reinvent the wheel" for solving a problem more elegantly, so bear with me. When it came out, I met a lot of other people in college who were making websites using Dreamweaver. They didn't know HTML, and they didn't think they needed to learn it. For awhile, I felt like an idiot, writing HTML code by hand in a basic text editor. But I took pride in clean code that rendered as expected (which, at the time, was enough of a challenge), loaded quickly and was easy to maintain.
I did learn Dreamweaver at one point. But I always dug into the source code it output and cleaned it up substantially by hand. Eventually, Dreamweaver became a dinosaur, and my proficiency with HTML, CSS and JS left me un-extinct.
Anyway, this is all to say there's fun and beauty to be found in the process of crafting the right solution yourself. And it shouldn't come as a surprise that the guy who's writing the World's Longest Blog about spending a year making his Own Running App, just for himself, prefers to write his own solutions. Aside over.
One of the first decisions I had to make was how to store persistent data locally on the user's phone. With Beer Dude, I could get by using UserDefaults for storing a few things like the user's name, high score and preferences. But the running app was going to require something more robust, like a relational database.
I first tried using Apple's Core Data framework for saving and retrieving run data. It felt needlessly obtuse though. Like, if you go to the store and ask for some eggs, and they're like, "yeah, ok, but first you have to fill out an Egg Requisition Form." And you're like, ok, where do I get that? And they're like, "it's been depreciated." But like, you can literally see the eggs behind the counter, so you ask nicely, and they're all, "oh, I'm sorry, those eggs have also been depreciated. Here's a watermelon."
Somewhere in this process I learned that Core Data is built on top of SQLite, which is available by default in all apps. Awesome.
Coming from my experience in MySQL from web development, I was in familiar territory with tables, columns and queries. SQLite does have some weird stuff to work around, such as limited data types and interfacing in C (you won't be surprised that I opted not to use the popular open source Swift wrapper SQLite.swift). But without much trouble, I wrote my own Database class (I always love a good singleton!) which handles all the details for me.
Building the cloud syncing system was a Challenge. The basic task of sending data to and from a server is easy. But there are so many cases to consider when building a system that syncs data across multiple devices. I will spare you the details here, but eventually I got it. The system got to a point where my runs were syncing reliably without fail. Nothing I could do would cause a sync error or a run to get lost or duplicated. Awesome.
Into the weeds with GPS
Another new area for me with this app was dealing with GPS locations. Despite trying to keep things simple and fun, I was forced to get into the absolute depths of the Core Location framework in iOS as well as the whole system of hardware components that are responsible for enabling the iPhone to make its best guess at a seemingly simple question: where am I?
The key word in that last sentence is "guess." The phone is very good at quickly making a guess about where it is. And it will usually give you a decent rough estimate.
The thing with a running app though, is that in order to get a precise measurement of your current pace or overall distance, you need data that is far more accurate than the phone is able to provide.
Here's an example. In ideal conditions, your phone can guess your location to within about 5 meters (about 16 feet). Let's take two points, A and B, which are 50 feet apart. The green circles below represent your phone's best guess as to your actual position at points A and B:
In reality though, you could be anywhere in either green circle. So while your phone thinks you traveled 50 feet, the actual distance from points A and B could be anywhere from about 40-60 feet. In less than ideal conditions, the green circle is larger, greatly increasing the range of possible distance between the points.
Let's say it took you 5 seconds to run from point A to B. This is something your phone is good at knowing, down to a very precise fraction of a second. But because it's not totally sure how far you ran, it could calculate your pace as anywhere from 11 min/mile (for 40 feet) to 7:20 min/mile (60 feet).
If you've ever used any running app on your phone or watch and felt like your current pace was fluctuating wildly, this is exactly why.
Another problem is that even if you run in a straight line, your phone might interpret your path to be more of a zig-zag. This happens more often in downtown areas with tall buildings or in the woods. But it looks like this:
So what happens is you think you ran a little farther than you actually did. This is why you might finish a 5K and your watch says 3.3 miles, or it says 13.5 at the end of a half marathon. There are other factors that go into this for sure, but it's an inherent problem with GPS path tracking.
One other fun detail is that when you measure the distance between two GPS points, you need to consider the curvature of the earth. Fortunately, the haversine formula exists. Core Location handles this for you, but on my web app, I had to implement this myself, and every example I found online produced a slightly different measurement from each other. So again, into the weeds we go, and in there you'll need to figure out, along with a lot of other fun obscurities, just how many decimal places are needed for an absolutely accurate meters-to-feet conversion.
Navigation apps like Google Maps can automatically place you on a road, straightening out the zig-zag greatly. But for running, you might not stick to a pre-defined path. And if you straighten it out too much, you start to lose distance on tight turns (90 degree turns are almost always undercut in running apps).
There's a good deal of averaging / sampling / predictive math you can do in your app to try and minimize the fluctuating nature of GPS inaccuracies, but at the end of the day, it's far from perfect, and you have to live with that.
One last point about GPS on the iPhone (I'm just kidding, I'll get into it more later): while the iPhone does have a GPS chip, its reported location uses a combination of data from other hardware (cell tower location, available wifi networks, accelerometer, …). This provides some benefits over pure GPS sometimes, but it comes with its own oddities. And there's no way as a developer to tell the phone to just use GPS. This applies to the Apple Watch as well. There are many GPS-based running watches that do a better job tracking runs accurately. But can you use them to play Clairo or live-blog your first marathon attempt? No. This is why I like running with a giant phone strapped to my arm. It's cumbersome and only semi-accurate, but its usefulness in other ways is unmatched.
Native app vs web app
Development of the native iPhone app was coming along well when I was forced to make a critical decision: should the native app do everything the web app could do? My initial thought was, yes, of course, let's make the native app the one and only app I need for this. It should log my runs and show me all the stats I need.
This isn't a bad decision, and if my goal was to release this to the public, the app would need to have everything built in.
But after a lot of thought, I decided to keep the native app very simple, using it just to log runs.
My reasoning was that I was constantly changing the web app — what data was displayed, how it was displayed, adding new features. With the web app, I could iterate quickly. But iPhone development is much, much slower. And I didn't want to abandon the power of a website on a desktop computer.
So I decided to focus the iPhone app on logging runs and sending them to the cloud, while focusing the web app on displaying stats. After months of using this dual-app workflow, I am so happy I went with this system. The web app has changed so much since early summer, incorporating things like goals and editable maps and advanced share sheets and many more charts and graphs. The native app continues to be refined for what it does best, logging runs. And they live very happily together on my home screen.
When I decided to keep the two apps separate, I also decided to drop multi-user support. I wasn't going to make an Android version. I didn't need it to interface with Garmins or Apple Watches or other wearables. I liked running with my phone, so I just made it work for me.
I took a look at the website, and the few people who had tested it out had predictably stopped using it months ago. I quietly deactivated the login page and waited to see if anyone would notice, and weblogs and a lack of emails assured me it went unnoticed.
It's really quite ridiculous (and borderline luxurious) to have this whole running ecosystem, including a fairly advanced native app, just for my use.
I had a ton of fun working on the user interface design for the native app. I used the same font (Avenir Next) and color scheme as the web app, and where it made sense, I borrowed the same minimalist design conventions.
The main screen on the app features a simplified version of that "daily runs" chart I mentioned earlier, but with absolutely no labels. It displays the last 60 days, represented either by a yellow bar (with the height representing the distance of the run), or a space (indicating no run that day). And the bars animate in such a fun way, but I have no idea how to show that here.
The "active run" interface has changed a lot since the first version. I prioritized displaying stats I cared about most in easy-to-read (while running) font sizes. At the top, it shows the temperature at the start of the run (it will average out the temp and humidity at the end of the run and save those to the cloud). It also displays the time, which is redundant since it's in the status bar, but it's really hard to read text that small while running with a phone in an armband covered with sweat and fog.
The time display serves a hidden second function as well — every now and then, for no good reason, music will stop playing over my AirPods. My left AirPod is set for double-tap to play/pause, but it's not totally reliable. So I made the time display a button, and when I press it, it'll start playing music.
In the top-right corner is a button to toggle interval alerts. My app uses the system voice to relay stats during a run. In the settings, I can choose to alert on intervals based on time or distance. But sometimes during a run, I don't want to know. I just want to run for awhile.
The audio interval alerts were incredibly easy to implement, as Apple has great tools for converting text to speech. There were a lot of details to consider though, such as shaping how certain things were pronounced. I wanted a pace of 7:54 to be "seven minutes, fifty-four seconds per mile," which the voice did by default. But if my pace was 8:00, it said, "eight o'clock."
The miles, time and pace on the display are self-explanatory. The current pace does its best job to show me how fast I'm running now, though in reality, it's how fast I've been running the past 20 seconds or so. It's not super accurate, but sometimes it comes in handy.
But way more reliable than current pace is a stat not displayed on the phone but conveyed via speech: the pace for the last interval. I find this to be way more valuable than current pace, and I can't imagine running without it now. While the current pace can fluctuate quite a bit, I can usually trust the last interval's pace. So if I'm 8 miles in on a run with an overall pace of 7:45, and my last interval is 7:56, I know I'm starting to slow down. And many times I don't realize I'm slowing down before the phone does.
The "10.0" near the bottom of the display is the accuracy of the latest GPS signal. It's in meters, and it usually fluctuates between 5 and 15. I didn't need to display this with my iPhone 6s+, but when I upgraded to an iPhone XS, my GPS points were all over the place.
Very long story short, the iPhone XS has a weakness where if the antenna gap on the side of the phone is bridged, the accuracy of the GPS signal drops significantly (it's Antennagate 2!). The armband I use for the phone kind of bridges the gap, and as it becomes saturated with sweat during a run, the signal becomes more and more degraded. So I keep my eye on this and usually end up holding the phone in my hand later in a run to get more accurate location.
The "stop" switch at the bottom of the interface is one of my favorite user interface success stories for the app. At first, this was a simple "stop" button (I didn't even have "pause" functionality on the first several versions — it stopped the run entirely when pressed). But during a run, especially in the summer, the armband for my phone would gather condensation under the screen protector, which would sometimes trigger random touches on the display. After the "stop" button was pressed by one of these phantom touches, I knew I'd need a better solution for stopping runs.
I checked out what other running apps did for their stop/pause buttons, and there was a mixture of solutions. Nike tried to solve this by requiring a long press on the pause button, but I didn't want to introduce a delay. Besides, it's perfectly conceivable that a phantom touch could remain active for a few seconds.
Turns out Apple already solved this problem on the very first iPhone with the (patented) "slide to unlock" switch on the home screen. They don't use this anymore, and it's not a standard interface element in Xcode, so I had to build my own. The details of getting this switch right are worth their own blog, but I finally got it, and it works perfectly. I can always end a run with confidence at the exact moment that I want to, and I know that this will never be activated by accident.
Before I solved this problem though, I realized I could probably ask Siri to stop my runs for me. I spent a lazy Saturday morning implementing Siri to do just that — and it works great. If a run is active, I say "Hey Siri stop running," and it stops. I almost never use this in practice, but on the rare occasion where I'm so sweaty the phone won't even recognize my fingers, it's nice to have.
The remaining screens in the app are fairly straightforward. My "get ready to run" screen features a large circle that starts red and changes to yellow and finally green as an accurate GPS signal is acquired. I could just have it start the run immediately, but it takes some time for the phone to narrow in on an accurate location.
The app is currently at version 32.1, and I'm still working on dialing in some details on location tracking. But it works, it works well, and I have no desire to go back to Runkeeper or use anything else.
The web app
The web app has changed a lot since its launch in October 2018. Scroll back up to the first image in the blog and you can see a few changes from the current interface below.
One major feature I added this summer was a "Goal" section. When 2019 started, my goal was to run more miles than I did in 2018 (323 miles). After I blew by that halfway through the year, I knew I needed a new goal. One of my friends told me about a Facebook group of local runners aiming for 1,000 miles for the year, so I joined and made that my goal.
I realized quickly I needed a new interface to track my progress towards this goal. I needed to consider weekly miles, something I hadn't even calculated before. I added some more charts and a whole new section to the interface showing how I'm doing on progress towards the goal.
More charts without labels! My favorite.
On the left is monthly miles with a dotted line showing the average miles per month I would need to achieve 1,000 miles for the year. It was immediately apparent just how behind I was as the start of July when I decided to hit 1,000 miles. I was coming off a slow June where travel and illness made running difficult.
In the middle, the "arc" chart shows the target distance for the current week, which is based on how many miles remain divided by how many weeks are left in the year. And on the right, the horizontal bars represent the percentage of miles toward the goal and the percentage of the year that has elapsed. When I started, the top bar was way behind the bottom, but I've since caught up and am currently ahead of schedule.
Above is the interface when viewing a single run. It's grown quite a bit as well, and I'm not sure I won't completely redesign it soon. But for now, it works, and it tells me what I want to know about a run. All of the various data comes together on this page, from GPS data to run metrics to settings about my weight and age to historical run data to the route map.
When you click the route map on the run details page, it opens up the editor above, which lets me see each individual GPS point that was recorded on the run. I can adjust them if needed and see the mileage change as I do. I can also select any two points to see the distance and pace between them. This interface is a little rough — I am still trying to figure out what else I might want to integrate into this view. But it's incredibly useful when troubleshooting (and fixing) GPS issues.
A little more on GPS
After building the interface above that lets me edit GPS points from a run, I was noticing that my corrected routes were a little shorter than what was recorded during the run. This means that sometimes my 10 mile runs were really 9.8 miles, and consequently, my pace was a little slower than I thought.
The main culprit for this was zig-zag points while running in a straight line. If I set Core Location to update less frequently (using distanceFilter), it would help straighten out the line, though it would undercut my mileage around tight corners. I wanted to come up with something a little more intelligent.
The iPhone is capable of giving you new location updates about every second, which is overkill for tracking runs and for battery life. I've had an old iPhone 6s+ with its original battery for most of the year, and it could barely last a half marathon sometimes.
But after getting a new phone with a fresh battery, I am now testing out some stuff where I acquire GPS points as often as possible. I store them in a queue, and when I have about 4-10 points, I average them together for the "official" GPS point.
The current experiment is to dynamically adjust the number of samples for each averaged point based on their horizontal accuracy. This is working well so far, but there's a lot of testing to be done. Which means more running! Yay!
To sum up and move on with life
So long story short, I made myself a running app. It's really fun, both as an effective tool to help with running and a real-world programming challenge.
My biggest challenge yet, both physically and with the app, is to complete a marathon-distance run. Which I am attempting to do tomorrow.
My body and phone battery should be able to make it. My two-year-old AirPods definitely will not. I've charted a route around Springfield that includes a few loops that go by my house, where I'll set out some drinks, snacks and the AirPod charging case.
I wanted Hannah to be able to meet me at the "finish line" (ie, the sidewalk) and track my progress, so on a whim in about an hour, I added a new feature to track my runs in real-time. I know you're literally on the edge of your seat, so if you happen to be reading this on Saturday morning, you can see how I'm doing here: https://run.daveheinzel.com/live/
So that's it. My little running app that has taken the better part of a year to develop. Just in time for me to cut back on running and focus more on learning the violin.