How To Make an App Like RunKeeper with Swift: Part 2

How To Make an App Like RunKeeper with Swift: Part 2
Track distance and achievements as you build a run-tracking app!

Track distance and achievements as you build a run-tracking app!

Update note: This tutorial was updated to Swift by Zouhair Mahieddine. The original Objective-C tutorial was written by Tutorial Team member Matt Luedke.

This is the second and final part of a tutorial that teaches you how to create an app like RunKeeper called MoonRunner, complete with color-coded maps and badges!

In the first part of the tutorial, you created an app that:

  • Uses Core Location to track your route
  • Continually maps your path and reports your average pace as you run
  • Shows a map of your route when the run is complete; the map line is color-coded so that the slower portions are red and the faster portions are green

That app is great for recording and displaying data, but to spark that motivation, sometimes you just need a little more of a nudge than a pretty map can provide.

In this part, you’ll complete the MoonRunner app with a badge system that embodies the concept that fitness is a fun and progress-based achievement. This will help keep your users motivated and coming back to using your app to keep track of their achievements.

Are you ready to unlock your own achievement of completing part two of this tutorial? Read on!

Getting Started

If you haven’t gone through part one of this tutorial (or if you cannot find the project’s code anymore; it happens :]), here is the final project from part one for you to start with.

The starter project already included a JSON file with details on all the badges – you can open badges.json and have a look if you’re curious.

The badges go all the way from 0 meters — hey, you have to start somewhere — up to the length of a full marathon. Of course, some people choose to go even further on ultra-marathons, and you can consider those ambitious runners as having entered interstellar space! :]

The first task is to parse the JSON file into an array of objects. Select File\New\File… and iOS\Source\Swift File and name it Badge.

Then, replace the contents of Badge.swift with the following:

import Foundation
class Badge {
  let name: String?
  let imageName: String?
  let information: String?
  let distance: Double?
  init(json: [String: String]) {
    name = json["name"]
    information = json["information"]
    imageName = json["imageName"]
    distance = (json["distance"] as? NSString)?.doubleValue

The Badge data fields are all optional since the passed Dictionary might not contain all the listed keys.

Now you have your Badge, and it’s time to parse the source JSON. Still in Badge.swift, create another class named BadgeController by adding the following code:

class BadgeController {
  static let sharedController = BadgeController()
  lazy var badges : [Badge] = {
    var _badges = [Badge]()
    let filePath = NSBundle.mainBundle().pathForResource("badges", ofType: "json") as String!
    let jsonData = NSData.dataWithContentsOfMappedFile(filePath) as! NSData
    var error: NSError?
    if let jsonBadges = NSJSONSerialization.JSONObjectWithData(jsonData,
     options: NSJSONReadingOptions.AllowFragments, error: &error) as? [Dictionary<String, String>] {
      for jsonBadge in jsonBadges {
        _badges.append(Badge(json: jsonBadge))
    else {
    return _badges

Here, you declare a singleton to access the same instance of the BadgeController from everywhere in your code. Then, you declare a lazy Array of Badge objects. When first accessed, the Array will be instantiated and populated using the data from Badges.json.

And with that, you’re all set to retrieve the badge data. Time to get that data incorporated into your app!

Earning The Badge

You’ve already created the Badge, and now you need an object to store when a badge was earned. This object will associate a Badge with the various Run objects, if any, where the user achieved versions of this badge.

Open Badge.swift and add the following code at the end of the file:

class BadgeEarnStatus {
  let badge: Badge
  var earnRun: Run?
  var silverRun: Run?
  var goldRun: Run?
  var bestRun: Run?
  init(badge: Badge) {
    self.badge = badge

Now that you have an easy way to associate a Badge with a Run, it’s time to build the logic to make those associations. Add the following constants at the top of Badge.swift:

let silverMultiplier = 1.05 // 5% speed increase
let goldMultiplier = 1.10 // 10% speed increase

The constants silverMultiplier and goldMultiplier are the factors by which a user has to speed up to earn those versions of the badge.

Then, add the following method to the BadgeController class:

func badgeEarnStatusesForRuns(runs: [Run]) -> [BadgeEarnStatus] {
  var badgeEarnStatuses = [BadgeEarnStatus]()
  for badge in badges {
    let badgeEarnStatus = BadgeEarnStatus(badge: badge)
    for run in runs {
      if run.distance.doubleValue > badge.distance {
        // This is when the badge was first earned
        if badgeEarnStatus.earnRun == nil {
          badgeEarnStatus.earnRun = run
        let earnRunSpeed = badgeEarnStatus.earnRun!.distance.doubleValue / badgeEarnStatus.earnRun!.duration.doubleValue
        let runSpeed = run.distance.doubleValue / run.duration.doubleValue
        // Does it deserve silver?
        if badgeEarnStatus.silverRun == nil && runSpeed > earnRunSpeed * silverMultiplier {
          badgeEarnStatus.silverRun = run
        // Does it deserve gold?
        if badgeEarnStatus.goldRun == nil && runSpeed > earnRunSpeed * goldMultiplier {
          badgeEarnStatus.goldRun = run
        // Is it the best for this distance?
        if let bestRun = badgeEarnStatus.bestRun {
          let bestRunSpeed = bestRun.distance.doubleValue / bestRun.duration.doubleValue
          if runSpeed > bestRunSpeed {
            badgeEarnStatus.bestRun = run
        else {
          badgeEarnStatus.bestRun = run
  return badgeEarnStatuses

This method compares all the user’s runs to the distance requirements for each badge, making the associations and returning all the BadgeEarnStatus objects in an array.

To do this, the first time a user achieves the badge, the method determines an earnRunSpeed, which becomes a reference to see if the user improved enough to qualify for the silver or gold versions.

Think of it this way – you’ll still have a chance at earning the gold version of the Mars badge, even if your friend is much faster every time. As long as you’re both making progress, you both win.


Note: You can see this method uses the overall average speed of a run. For a little extra challenge, try working with more intensive math to also use portions of a run to calculate speeds. For example, your fastest mile could have been in the middle of a two-mile run with a slow beginning and end.

Displaying the Badges

Now it’s time to bring all this badge logic and UI together for the user. You’re going to create two view controllers and one custom table cell in order to link the storyboards with the badge data.

Add a new Swift File to the project named BadgeCell. Open the new file, and replace its contents with the following:

import UIKit
import HealthKit
class BadgeCell: UITableViewCell {
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var descLabel: UILabel!
  @IBOutlet weak var badgeImageView: UIImageView!
  @IBOutlet weak var silverImageView: UIImageView!
  @IBOutlet weak var goldImageView: UIImageView!

Now you have a custom cell to use in the table view controller for badges.

Next, create another Swift File named BadgesTableViewController. Open the new file and replace its contents with the following:

import UIKit
import HealthKit
class BadgesTableViewController: UITableViewController {
  var badgeEarnStatusesArray: [BadgeEarnStatus]!

The badgeEarnStatusesArray will be the result of calling badgeEarnStatusesForRuns(_:) in the BadgeController — the method you added earlier to calculate badge statuses.

Then add the following properties to the class:

let redColor = UIColor(red: 1, green: 20/255, blue: 44/255, alpha: 1)
let greenColor = UIColor(red: 0, green: 146/255, blue: 78/255, alpha: 1)
let dateFormatter: NSDateFormatter = {
  let _dateFormatter = NSDateFormatter()
  _dateFormatter.dateStyle = .MediumStyle
  return _dateFormatter
let transform = CGAffineTransformMakeRotation(CGFloat(M_PI/8.0))

These are a few properties that will be used throughout the table view controller. The colors will be used to color each cell according to whether or not the badge has been earned, for example.

The properties are essentially caches so that each time a new cell is created you don’t need to recreate the required properties over and over. Date formatters are especially expensive to create and therefore a good idea to create once and re-use multiple times if you can.

Then, add the following extension to the bottom of the file to implement the UITableViewDataSource protocol:

// MARK: - UITableViewDataSource
extension BadgesTableViewController {
  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return badgeEarnStatusesArray.count
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("BadgeCell") as! BadgeCell
    let badgeEarnStatus = badgeEarnStatusesArray[indexPath.row]
    cell.silverImageView.hidden = (badgeEarnStatus.silverRun != nil)
    cell.goldImageView.hidden = (badgeEarnStatus.goldRun != nil)
    if let earnRun = badgeEarnStatus.earnRun {
      cell.nameLabel.textColor = greenColor
      cell.nameLabel.text =!
      cell.descLabel.textColor = greenColor
      cell.descLabel.text = "Earned: " + dateFormatter.stringFromDate(earnRun.timestamp)
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.silverImageView.transform = transform
      cell.goldImageView.transform = transform
      cell.userInteractionEnabled = true
    else {
      cell.nameLabel.textColor = redColor
      cell.nameLabel.text = "?????"
      cell.descLabel.textColor = redColor
      let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
      cell.descLabel.text = "Run \(distanceQuantity.description) to earn"
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.userInteractionEnabled = false
    return cell

These methods tell the table view how many rows to show (the number of badges) and how to set up each cell. As you can see, every cell depends on if the user earned the badge, among other things. Also, the cell can only be selected if the badge has been earned, through the use of userInteractionEnabled.

Now you need to provide the BadgesTableViewController some data. Open HomeViewController.swift and add the following code to prepareForSegue(_:sender:):

else if segue.destinationViewController.isKindOfClass(BadgesTableViewController) {
  let fetchRequest = NSFetchRequest(entityName: "Run")
  let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false)
  fetchRequest.sortDescriptors = [sortDescriptor]
  let runs = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as! [Run]
  let badgesTableViewController = segue.destinationViewController as! BadgesTableViewController
  badgesTableViewController.badgeEarnStatusesArray = BadgeController.sharedController.badgeEarnStatusesForRuns(runs)

Here, when the BadgesTableViewController is being pushed onto the navigation stack, the earn status of all badges is calculated and passed to the badge table view controller.

Now it’s time to connect all your outlets in the storyboard. Open Main.storyboard and do the following:

  • Set the classes of BadgeCell and BadgesTableViewController
  • Connect the outlets of BadgeCell: nameLabel to the “Name” label, descLabel to the “Earned” label, badgeImageView to the square image view on the left, and silverImageView and goldImageView to the silver and gold spaceship images.

Build and run and check out your new badges!


If you’ve taken at least one run already, you should have at least the Earth badge. It’s a start – now to grab some more badges!

Badge Details

The last view controller for MoonRunner is the one that shows the details of a badge. Create a new Swift File called BadgeDetailsViewController and replace its contents with the following:

import UIKit
import HealthKit
class BadgeDetailsViewController: UIViewController {
  var badgeEarnStatus: BadgeEarnStatus!
  @IBOutlet weak var badgeImageView: UIImageView!
  @IBOutlet weak var silverImageView: UIImageView!
  @IBOutlet weak var goldImageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var distanceLabel: UILabel!
  @IBOutlet weak var earnedLabel: UILabel!
  @IBOutlet weak var silverLabel: UILabel!
  @IBOutlet weak var goldLabel: UILabel!
  @IBOutlet weak var bestLabel: UILabel!

The class will need to store the earn status of the selected badge, in addition to all the outlets for the view.

Next, add the following method to the class to set up the view:

override func viewDidLoad() {
  let formatter = NSDateFormatter()
  formatter.dateStyle = .MediumStyle
  let transform = CGAffineTransformMakeRotation(CGFloat(M_PI/8.0))
  nameLabel.text =
  let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
  distanceLabel.text = distanceQuantity.description
  badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
  if let run = badgeEarnStatus.earnRun {
    earnedLabel.text = "Reached on " + formatter.stringFromDate(run.timestamp)
  if let silverRun = badgeEarnStatus.silverRun {
    silverImageView.transform = transform
    silverImageView.hidden = false
    silverLabel.text = "Earned on " + formatter.stringFromDate(silverRun.timestamp)
  else {
    silverImageView.hidden = true
    let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
    let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
    silverLabel.text = "Pace < \(paceQuantity.description) for silver!"
  if let goldRun = badgeEarnStatus.goldRun {
    goldImageView.transform = transform
    goldImageView.hidden = false
    goldLabel.text = "Earned on " + formatter.stringFromDate(goldRun.timestamp)
  else {
    goldImageView.hidden = true
    let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
    let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
    goldLabel.text = "Pace < \(paceQuantity.description) for gold!"
  if let bestRun = badgeEarnStatus.bestRun {
    let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
    let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: bestRun.duration.doubleValue / bestRun.distance.doubleValue)
    bestLabel.text = "Best: \(paceQuantity.description), \(formatter.stringFromDate(bestRun.timestamp))"

This code sets up the badge image and puts all the data about the badge earning into the labels.

The most interesting parts are the prompts that tell the user how much they need to speed up to earn the coveted silver and gold badges. I’ve found these prompts to be very motivating, as the pace is always an improvement that requires effort but is obviously possible.

Finally, add the following method:

@IBAction func infoButtonPressed(sender: AnyObject) {
            message: badgeEarnStatus.badge.information!,
           delegate: nil,
  cancelButtonTitle: "OK").show()

This will be invoked when the user taps the info button. It shows a pop-up with the badge’s information.

Now that the detail view is set up, you’ll need to make sure the badges table view sends the badge information over before the segue. Open BadgesTableViewController.swift and add the following method to the BadgesTableViewController class:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
  if segue.destinationViewController.isKindOfClass(BadgeDetailsViewController) {
    let badgeDetailsViewController = segue.destinationViewController as! BadgeDetailsViewController
    let badgeEarnStatus = badgeEarnStatusesArray[tableView.indexPathForSelectedRow()!.row]
    badgeDetailsViewController.badgeEarnStatus = badgeEarnStatus

This sets up the segue for when a cell is tapped. It hands over the relevant BadgeEarnStatus instance for the BadgeDetailsViewController to display.

Great! Now the code side of the badge UI is all set. Open Main.storyboard and make the following connections:

  • Set the BadgeDetailsViewController class.
  • Connect the outlets of BadgeDetailsViewController: badgeImageView, bestLabel, distanceLabel, earnedLabel, goldImageView, goldLabel, nameLabel, silverImageLabel, and silverLabel.
  • Connect the infoButtonPressed(_:) action to the blue info button’s Touch Up Inside event.

Now build and run and check out your new badge details!


Badge Motivation

Along with the new portion of the app devoted to badges, you need to go back through the UI of the existing app and update it to incorporate the badges!

Open Main.storyboard and find the New Run scene. Add a UIImageView and a UILabel to its view, somewhere above the Stop button.

For the UIImageView, use the following Auto Layout constraints:

  • Align Center X to Superview
  • Width equals 70
  • Height equals 70
  • Align top with the Start button

For the UILabel, use the followings:

  • Align Center X to: Superview
  • Top Space to: UIImageView equals 10

The New Run scene should now look like this:


The new views overlap with the start button, but the idea is that you’ll hide the start button and show these two views once the run starts.

These will be used as a “carrot-on-a-stick” type motivator for the user as they run, giving them a sneak peak at the next badge and how much farther away it is.

Before you can hook up the UI, you need to add a couple methods to BadgeController to determine which badge is best for a certain distance, and which one is coming up next.

Open Badge.swift and add the following methods to the BadgeController class:

func bestBadgeForDistance(distance: Double) -> Badge {
  var bestBadge = badges.first as Badge!
  for badge in badges {
    if distance < badge.distance {
    bestBadge = badge
  return bestBadge
func nextBadgeForDistance(distance: Double) -> Badge {
  var nextBadge = badges.first as Badge!
  for badge in badges {
    nextBadge = badge
    if distance < badge.distance {
  return nextBadge

These are fairly straightforward — they each take an input, distance in meters, and return either:

  • bestBadgeForDistance(_:): The badge that was last won.
  • nextBadgeForDistance(_:): The badge that is next to be won.

Now open NewRunViewController.swift and add the following import at the top of the file:

import AudioToolbox

You add the AudioToolbox import so that you can play a sound every time the user earns a new badge.

Then, add these properties to the NewRunViewController class:

var upcomingBadge : Badge?
@IBOutlet weak var nextBadgeLabel: UILabel!
@IBOutlet weak var nextBadgeImageView: UIImageView!

Then find viewWillAppear(_:) and add the following code at the end of the method:

nextBadgeLabel.hidden = true
nextBadgeImageView.hidden = true

Just like the other views, the badge label and badge image need to start out hidden.

Then find startPressed(_:) and add the following code at the end of the method:

nextBadgeLabel.hidden = false
nextBadgeImageView.hidden = false

This ensures that the badge label and badge image show up when the run starts.

Then, add these two new methods:

func playSuccessSound() {
  let soundURL = NSBundle.mainBundle().URLForResource("success", withExtension: "wav")
  var soundID : SystemSoundID = 0
  AudioServicesCreateSystemSoundID(soundURL, &soundID)
  //also vibrate
func checkNextBadge() {
  let nextBadge = BadgeController.sharedController.nextBadgeForDistance(distance)
  if let upcomingBadge = upcomingBadge {
    if! !=! {
  upcomingBadge = nextBadge

The first method plays the success sound, and it also vibrates the phone using the system vibrate sound ID, in case the user is running in a noisy location, like next to a busy road. Or maybe they’ve got their music on and can’t hear the sound anyway!

The second one obtains the next badge using the method you added previously. It checks to see if this badge is new, by comparing the name to the existing upcoming badge store in upcomingBadge. If the badge is different, then a success sound is played because that means a new badge has been earned!

Now find eachSecond(_:) and add the following code at the end of the method:

if let upcomingBadge = upcomingBadge {
  let nextBadgeDistanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: upcomingBadge.distance! - distance)
  nextBadgeLabel.text = "\(nextBadgeDistanceQuantity.description) until \(!)"
  nextBadgeImageView.image = UIImage(named: upcomingBadge.imageName!)

This makes sure nextBadgeLabel and nextBadgeImageView are always up-to-date as the run progresses.

Open Main.storyboard and find the New Run scene. Connect the outlets for nextBadgeLabel and nextBadgeImageView.

Then build and run and start a new run. Watch the label and image update as you run! Stick around for the sound when you pass a new badge!


Where to go From Here

Congratulations, you’ve earned the “built a run-tracking app with location tracking and achievements” badge by finishing this tutorial! Here’s a download with the completed project.

Over the course of this two-part tutorial, you built an app that:

  • Measures and tracks your runs using Core Location
  • Displays real-time data, like the run’s average pace, along with an active map
  • Maps out a run with a color-coded polyline and custom annotations at each checkpoint
  • Awards badges for personal progress in distance and speed

Remember when building this kind of app, it’s not enough to just have the basic features such as mapping out the run – it’s all about user engagement and giving them a reason to come back to your apps. Badges are great way to “gamify” things and give your users a sense of accomplishment.

If you’re looking to take things to the next level, why not try adding some more features to the app?

  • Show the user’s past runs
  • Try using the speeds for segments of a run to earn badges
  • Display map annotations to show where the user earned badges

Thank you for reading! If you have questions, comments or want to share your successes, please post in the comments below! Enjoy running through the atmosphere and then beyond the Solar System! :]

The post How To Make an App Like RunKeeper with Swift: Part 2 appeared first on Ray Wenderlich.



Write a comment