How To Make an App Like RunKeeper: Part 1

How To Make an App Like RunKeeper: Part 1


At the recent WWDC 2014 conference, Apple previewed the upcoming HealthKit API and corresponding Health app. Meanwhile, apps in the Health and Fitness category are hugely popular on the App Store.

RunKeeper, a GPS app like the one you’re about to make, has over 25 million users! What’s clear from this growing trend is:

  • Health is extremely important
  • Smartphones can serve as useful aids for managing and tracking your health
  • Developing fun apps can help people stay motivated, on-track to achieve goals and most importantly, stay healthy!

This tutorial will show you how to make an app like RunKeeper. It will be a motivational run-tracking app. By the end of this tutorial, you’ll have an app that:

  • Uses Core Location to track your route.
  • Shows a map during your run, with a constantly-updating line denoting your path.
  • Continually reports your average pace as you run.
  • Awards badges for running various distances.
    • Silver and gold versions of each badge recognize personal improvements, regardless of your starting point.
  • Encourages you by tracking the remaining distance to the next checkpoint.
  • Shows a map of your route when you’re finished.
    • The map line is color-coded so that the slower portions are red and the faster portions are green.

The result? Your new app, called MoonRunner, with badges based on planets and moons in our Solar System! Yes, you and your users will be traveling through space to earn these badges :]

Before you run headlong into this tutorial, there’s some prerequisite skills and suggested prior reading:

  1. Storyboards: You’ll use Storyboards for the UI on this app, so read this excellent tutorial for a refresher.

  2. Core Data: Knowing where you start and measuring your progress are fundamental to personal training. So, to save data, you’ll use the most widely-used data persistence framework in Cocoa, Core Data. Here is an excellent Core Data tutorial to get you up to speed.
  3. While it’s not required, the Breadcrumb sample project from Apple contains some helpful (but currently deprecated) example code to show a map during the run. You’ll do the same from scratch later in this tutorial, but it doesn’t hurt to brush up on the topic of MapKit if you’re unfamiliar.

There’s so much to talk about that this tutorial comes in two parts. The first segment focuses on recording the run data and rendering the color-coded map. The second segment introduces the badge system.

Getting Started

The first thing to do, of course, is to make your new project.

Open Xcode. Select File\New\Project. Then select iOS\Application\Master-Detail Application.


Give it the name MoonRunner and make sure to check the Use Core Data option.


Just like that, you already have a template project built with Core Data and storyboards!

Model: Runs and Locations

Your usage of Core Data in this app is fairly minimal, with only two entities: Runs and Locations.

Open MoonRunner.xcdatamodeld. Then delete the existing Event entity by clicking on it and pressing backspace. Then add two new entitites, called Run and Location. Set up Run with the following properties:


A Run contains a duration, a distance and a timestamp. It also has a relationship called locations that relates to your other object:

Now set up Location with the following properties:


A Location contains a latitude and longitude, as well as a timestamp. Additionally, Locationrelates back to a Run.

Be sure to set the locations relationship as a to-many, ordered relationship.


Next, have Xcode generate the model classes. Click File\New\File and then choose Core Data\NSManagedObject subclass.


When the follow-up options present themselves, be sure to include the model for MoonRunner and both Run and Location.



Alright! And just like that, you’ve completed the Core Data piece of this app.

This tutorial will make use of MapKit, so it needs to be linked. Click on the project at the top of the project navigator and open the target’s Build Phases. Add MapKit to the list inside the Link Binary With Libraries section:


Setting Up the Storyboards

Time to move on to some visuals! First up is laying out the storyboard. Remember, if the following is new to you, or you’re experiencing a temporary memory lapse, refer to the Storyboards tutorial for a bit of help with the concepts.

Open Main.storyboard.

For this first set of features, MoonRunner has a simple flow:

  • A Home screen that presents a few app navigation options
  • A New Run screen where the user begins and records a new run
  • A Run Details screen that shows the results, including the color-coded map

None of these are a UITableViewController, so click on the black bar below the pre-made Master View Controller to select it, then press delete.


Then, add two new view controllers by dragging two from the object library. Name one of them ‘Home’ by selecting the black bar again, and then setting the title in the attributes inspector. Name the other ‘New Run’.

Control-drag between the Navigation Controller to Home and set the relationship as ‘root view’.


Then control-drag from the yellow icon on the New Run view controller’s black bar to the Detail screen and set the segue to push.


Click on the segue that you just added. Then in the Attributes Inspector, assign the name ‘RunDetails’ to the segue.


Behold…your Storyboards:


Now you’re going to lay out each of the screens.

Fleshing out the Storyboards

Please download the resource package. Then unzip the package and drag all the files into project. Make sure you select “Copy items into destination group’s folder (if needed)”.

Open Main.storyboard and find the ‘Home’ view controller.

The ‘Home’ view controller is the main menu for your app. Drag in a UILabel to the top of the View, and add a warm, welcoming message like ‘Welcome To MoonRunner!’

Then drag in three UIButtons, and give them the titles ‘New Run’, ‘Past Runs’ and ‘My Badges’.

In the starter pack there are green-btn and blue-btn assets, and I used a black background withwhite text to make the app look like this:


Then, control-drag from the ‘New Run’ button to the ‘New Run’ screen, and select a push segue:


The New Run screen has two modes: pre-run and during-run. The View Controller handles the logic of how each one displays, but for now drag out three UILabels: one each to display real-time updates of distance, time and pace.

Add a UILabel as a prompt (For example, “Ready To Launch?”), and two UIButtons to Start/Stop the run.

I used a black background and the green-btn and red-btn assets to provide a little styling. If you do the same, your storyboard will look like this:


Lastly, the Run Details screen is where the user sees a map of their route, along with other details that relate to a specific run.

Find the ‘Detail View Controller’ on the storyboard. Then delete the existing label and add an MKMapView, as well as four UILabels. The labels will be used for distance, date, time and pace.

With the usual, void-of-space-black background, your storyboard should look like this:


Great! You’ll come back to these to make a few connections, but for now it’s time to get a little control over things…with Controllers.

Completing the Basic App Flow

Xcode did a good chunk of the heavy lifting for you in the Master-Detail app template, but you don’t need what’s in the MasterViewController. So go ahead and delete MasterViewController.h and MasterViewController.m.

Then replace it with a new, fresh HomeViewController by choosing File\New\File and select the iOS\Cocoa Touch\Objective-C class template. Choose UIViewController as your subclass, and name the new class HomeViewController.

You’ll need to feed this View Controller an NSManagedObjectContext from the App Delegate, so add a new NSManagedObjectContext property to HomeViewController.h. It should now look like this:

#import <UIKit/UIKit.h>
@interface HomeViewController : UIViewController
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

Move back to AppDelegate.m for a moment — this file has errors since you removed MasterViewController. Oh no!

Actually, it’s not a big deal. Replace the import of MasterViewController.h at the top with this:

#import "HomeViewController.h"

Next, make application:didFinishLaunchingWithOptions: look like this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
    HomeViewController *controller = (HomeViewController *)navigationController.topViewController;
    controller.managedObjectContext = self.managedObjectContext;
    return YES;

Great! Now add another file by selecting File\New\File and select the iOS>Cocoa Touch>Objective-C class template. Call the file NewRunViewController, once again inheriting from UIViewController.

Make the same changes to NewRunViewController.h as you did to HomeViewController.h:

#import <UIKit/UIKit.h>
@interface NewRunViewController : UIViewController
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

Open NewRunViewController.m and add the following to the top of the file:

#import "DetailViewController.h"
#import "Run.h"
static NSString * const detailSegueName = @"RunDetails";
@interface NewRunViewController () <UIActionSheetDelegate>
@property (nonatomic, strong) Run *run;
@property (nonatomic, weak) IBOutlet UILabel *promptLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeLabel;
@property (nonatomic, weak) IBOutlet UILabel *distLabel;
@property (nonatomic, weak) IBOutlet UILabel *paceLabel;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@property (nonatomic, weak) IBOutlet UIButton *stopButton;

Now you have Interface Builder outlets for each of the views in your storyboard, as well as a constant string for the segue name.

Did you notice that you’ll need to implement the UIActionSheetDelegate protocol? You’ll do that very soon.

The next steps are to define the initial state of the UI and the actions related to button presses.

Add the following method to the main implementation body of NewRunViewController:

- (void)viewWillAppear:(BOOL)animated
    [super viewWillAppear:animated];
    self.startButton.hidden = NO;
    self.promptLabel.hidden = NO;
    self.timeLabel.text = @"";
    self.timeLabel.hidden = YES;
    self.distLabel.hidden = YES;
    self.paceLabel.hidden = YES;
    self.stopButton.hidden = YES;

The only things visible at first are the start button and the prompt. Next, add the following methods:

    // hide the start UI
    self.startButton.hidden = YES;
    self.promptLabel.hidden = YES;
    // show the running UI
    self.timeLabel.hidden = NO;
    self.distLabel.hidden = NO;
    self.paceLabel.hidden = NO;
    self.stopButton.hidden = NO;
- (IBAction)stopPressed:(id)sender
    UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"" delegate:self 
            cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil 
            otherButtonTitles:@"Save", @"Discard", nil];
    actionSheet.actionSheetStyle = UIActionSheetStyleDefault;
    [actionSheet showInView:self.view];

This code provides two actions to be used when the two buttons are pressed. Pressing the start button switches the UI into “during-run” mode, while pressing the stop button shows a UIActionSheet so the user can decide whether to save or discard the run.

Now you need to make sure something’s there to respond to the user’s choice on the UIActionSheet. Add the following method:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
    // save
    if (buttonIndex == 0) {
        [self performSegueWithIdentifier:detailSegueName sender:nil];
    // discard
    } else if (buttonIndex == 1) {
        [self.navigationController popToRootViewControllerAnimated:YES];

This will perform the detail segue if the ‘Save’ button is tapped, and pop to the root (i.e. the home screen) if the ‘Discard’ button is tapped.

Finally, add the following method:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    [[segue destinationViewController]];

This sets up the DetailViewController with the current run when the segue is triggered.

For now, the run is empty, but that’s because you haven’t taken or run — or finished the app for that matter. You can see where this is going though, that run will soon be constructed from all the locations read during the run.

You’re done with NewRunViewController.m for now!

Open DetailViewController.h. Make it look like this:

#import <UIKit/UIKit.h>
@class Run;
@interface DetailViewController : UIViewController
@property (strong, nonatomic) Run *run;

This adds a Run property to the detail view controller. In fact, the one you set in the segue just now! This will be the run that the detail view controller is set up to show details for.

Then open DetailViewController.m and replace the entire file with the following code:

#import "DetailViewController.h"
#import <MapKit/MapKit.h>
@interface DetailViewController () <MKMapViewDelegate>
@property (nonatomic, weak) IBOutlet MKMapView *mapView;
@property (nonatomic, weak) IBOutlet UILabel *distanceLabel;
@property (nonatomic, weak) IBOutlet UILabel *dateLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeLabel;
@property (nonatomic, weak) IBOutlet UILabel *paceLabel;
@implementation DetailViewController
#pragma mark - Managing the detail item
- (void)setRun:(Run *)run
    if (_run != run) {
        _run = run;
        [self configureView];
- (void)configureView
- (void)viewDidLoad
    [super viewDidLoad];
    [self configureView];

This imports MapKit, so that you can make use of MKMapView. It also adds outlets for the various parts of the UI. Then it sets up the basis for the rest of the code. A method that will configure the view for the current run, which is called when the view is loaded and when a new run is set.

There’s just one last step before you’re ready to return to your Storyboards and connect all your outlets. Open HomeViewController.m and add the following import at the top of the file:

#import "NewRunViewController.h"

Then add the following method in the implementation:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    UIViewController *nextController = [segue destinationViewController];
    if ([nextController isKindOfClass:[NewRunViewController class]]) {
        ((NewRunViewController *) nextController).managedObjectContext = self.managedObjectContext;

Finally, head back to your storyboard and set all the following (again, if you need a refresher on storyboards, refer here):

  1. Set the class of the Home View Controller to HomeViewController.
  2. Set the class of the New Run View Controller to NewRunViewController.
  3. Connect all the outlets of the NewRunViewController and DetailViewController.
  4. Connect both the received actions (startPressed: and stopPressed:) in NewRunViewController.
  5. Connect the MKMapView to DetailViewController as its delegate.

After you’ve done all that, you’ve hit the first checkpoint. Build and Run!


Your app should now have the very basics of its UI: you should be able to navigate between the three screens, and “start” and “stop” a run. Now it’s time to really get into the guts of the app!

Math and Units

Note that several of the views you created and attached in the storyboard involve displaying statistics and times. Core Location sensibly measures everything in the units of science, aka the metric system. However, a good chunk of your users live in the United States and use silly units like miles, so you need to make sure both systems are live in your app. :]

Click File\New\File. Select iOS\Cocoa Touch\Objective-C class. Call the file MathController, inheriting from NSObject and create it. Then open MathController.h and make the file look like this:

#import <Foundation/Foundation.h>
@interface MathController : NSObject
+ (NSString *)stringifyDistance:(float)meters;
+ (NSString *)stringifySecondCount:(int)seconds usingLongFormat:(BOOL)longFormat;
+ (NSString *)stringifyAvgPaceFromDist:(float)meters overTime:(int)seconds;

Then open MathController.m and add the following to the top of the file.

static bool const isMetric = YES;
static float const metersInKM = 1000;
static float const metersInMile = 1609.344;

Those in the US can change isMetric to NO if they wish! :]

Next, add the following methods to the implementation:

+ (NSString *)stringifyDistance:(float)meters
    float unitDivider;
    NSString *unitName;
    // metric
    if (isMetric) {
        unitName = @"km";
        // to get from meters to kilometers divide by this
        unitDivider = metersInKM;
    // U.S.
    } else {
        unitName = @"mi";
        // to get from meters to miles divide by this
        unitDivider = metersInMile;
    return [NSString stringWithFormat:@"%.2f %@", (meters / unitDivider), unitName];
+ (NSString *)stringifySecondCount:(int)seconds usingLongFormat:(BOOL)longFormat
    int remainingSeconds = seconds;
    int hours = remainingSeconds / 3600;
    remainingSeconds = remainingSeconds - hours * 3600;
    int minutes = remainingSeconds / 60;
    remainingSeconds = remainingSeconds - minutes * 60;
    if (longFormat) {
        if (hours > 0) {
            return [NSString stringWithFormat:@"%ihr %imin %isec", hours, minutes, remainingSeconds];
        } else if (minutes > 0) {
            return [NSString stringWithFormat:@"%imin %isec", minutes, remainingSeconds];
        } else {
            return [NSString stringWithFormat:@"%isec", remainingSeconds];
    } else {
        if (hours > 0) {
            return [NSString stringWithFormat:@"%02i:%02i:%02i", hours, minutes, remainingSeconds];
        } else if (minutes > 0) {
            return [NSString stringWithFormat:@"%02i:%02i", minutes, remainingSeconds];
        } else {
            return [NSString stringWithFormat:@"00:%02i", remainingSeconds];
+ (NSString *)stringifyAvgPaceFromDist:(float)meters overTime:(int)seconds
    if (seconds == 0 || meters == 0) {
        return @"0";
    float avgPaceSecMeters = seconds / meters;
    float unitMultiplier;
    NSString *unitName;
    // metric
    if (isMetric) {
        unitName = @"min/km";
        unitMultiplier = metersInKM;
    // U.S.
    } else {
        unitName = @"min/mi";
        unitMultiplier = metersInMile;
    int paceMin = (int) ((avgPaceSecMeters * unitMultiplier) / 60);
    int paceSec = (int) (avgPaceSecMeters * unitMultiplier - (paceMin*60));
    return [NSString stringWithFormat:@"%i:%02i %@", paceMin, paceSec, unitName];

These methods are helpers that convert from distances, time and speeds into pretty strings. I won’t explain them in too much detail because they should be self explanatory. If you want a challenge later, why not localize the numbers. Refer to this tutorial on the subject if you would like to do this.

Starting the Run

Do you remember who said, “Even a marathon begins with just a single CLLocationManager update.”? Probably no one — until just now

Next up, you’ll start recording the location of the user for the duration of the run.

You need to make one important project-level change first. Click on the project at the top of the project navigator. Then select the Capabilities tab and open up Background Modes. Turn on the switch for this section on the right and then tick Location Updates. This will allow the app to update the location even if the user presses the home button to take a call, browse the net or find out where the nearest Starbucks is! Neat!

Background modes

Did you know? If your app goes in the App Store, you’ll have to attach this disclaimer to your app’s description: “Continued use of GPS running in the background can dramatically decrease battery life.”

Now, back to the code. Open NewRunViewController.m. Then add the following imports at the top of the file:

#import <CoreLocation/CoreLocation.h>
#import "MathController.h"
#import "Location.h"

Next add the CLLocationManagerDelegate protocol conformance declaration and several new properties to the class extension category:

@interface NewRunViewController () <UIActionSheetDelegate, CLLocationManagerDelegate>
@property int seconds;
@property float distance;
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray *locations;
@property (nonatomic, strong) NSTimer *timer;
  • seconds tracks the duration of the run, in seconds.
  • distance holds the cumulative distance of the run, in meters.
  • locationManager is the object you’ll tell to start or stop reading the user’s location.
  • locations is an array to hold all the Location objects that will roll in.
  • timer will fire each second and update the UI accordingly.

Now add the following method to the implementation:

- (void)viewWillDisappear:(BOOL)animated
    [super viewWillDisappear:animated];
    [self.timer invalidate];

With this method, the timer is stopped when the user navigates away from the view.

Add the following method:

- (void)eachSecond
    self.timeLabel.text = [NSString stringWithFormat:@"Time: %@",  [MathController stringifySecondCount:self.seconds usingLongFormat:NO]];
    self.distLabel.text = [NSString stringWithFormat:@"Distance: %@", [MathController stringifyDistance:self.distance]];
    self.paceLabel.text = [NSString stringWithFormat:@"Pace: %@",  [MathController stringifyAvgPaceFromDist:self.distance overTime:self.seconds]];

This is the method that will be called every second, by using an NSTimer (which will be set up shortly). Each time this method is called, you increment the second count and update each of the statistics labels accordingly.

And guess what? Another method! Add the following:

- (void)startLocationUpdates
    // Create the location manager if this object does not
    // already have one.
    if (self.locationManager == nil) {
        self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    self.locationManager.activityType = CLActivityTypeFitness;
    // Movement threshold for new events.
    self.locationManager.distanceFilter = 10; // meters
    [self.locationManager startUpdatingLocation];

If needed, you make a CLLocationManager, and set its delegate to this class so it knows where to send all the location updates.

You then feed it a desiredAccuracy of kCLLocationAccuracyBest. You may think this is silly; why ask for anything less than the best?

The device can make a precise reading with GPS hardware, but this is expensive in terms of battery usage. It can also do a lower-power, roughly accurate reading using radios that are already turned on, such as Wi-Fi or cell-tower readings.

Yes, the GPS route takes more battery, and there are some use cases for the less accurate readings, e.g. where you only need to verify that a user is in a general area. However, this app tracks your actual run, so it needs to be as accurate as possible.

The activityType parameter is made specifically for an app like this. It intelligently helps the device to save some power throughout a user’s run, say if they stop to cross a road.

Lastly, you set a distanceFilter of 10 meters. As opposed to the activityType, this parameter doesn’t affect battery life. The activityType is for readings, and the distanceFilter is for the reporting of readings.

As you’ll see after doing a test run later, the location readings can be a little zigged or zagged away from a straight line.

A higher distanceFilter could reduce the zigging and zagging and thus give you a more accurate line. Unfortunately, too high a filter would pixelate your readings. That’s why 10 meters is a good balance.

Finally, you tell the manager to start getting location updates! Time to hit the pavement. Wait! Are you lacing up your shoes? Not that kind of run!

To actually begin the run, add these lines to the end of startPressed::

self.seconds = 0;
self.distance = 0;
self.locations = [NSMutableArray array];
self.timer = [NSTimer scheduledTimerWithTimeInterval:(1.0) target:self
        selector:@selector(eachSecond) userInfo:nil repeats:YES];
[self startLocationUpdates];

Here all the fields that will update continually throughout the run are reset.

Recording the Run

You’ve created the CLLocationManager, but now you need to get updates from it. That is done through it’s delegate. Open NewRunController.m once again and add the following method:

- (void)locationManager:(CLLocationManager *)manager
     didUpdateLocations:(NSArray *)locations
    for (CLLocation *newLocation in locations) {
        if (newLocation.horizontalAccuracy < 20) {
            // update distance
            if (self.locations.count > 0) {
                self.distance += [newLocation distanceFromLocation:self.locations.lastObject];
            [self.locations addObject:newLocation];

This will be called each time there are new location updates to provide the app.

It updates quite often with an array of CLLocations. Usually this array only contains one object, but if there are more, they are ordered by time with the most recent location last.

A CLLocation contains some great information. Namely the latitude and longitude, along with the timestamp of the reading.

But before blindly accepting the reading, it’s worth a horizontalAccuracy check. If the device isn’t confident it has a reading within 20 meters of the user’s actual location, it’s best to keep it out of your dataset.

Note: This check is especially important at the start of the run, when the device first starts narrowing down the general area of the user. At that stage, it’s likely to update with some inaccurate data for the first few points.

If the CLLocation passes the check, then the distance between it and the most recent point is added to the cumulative distance of the run. The distanceFromLocation: method is very convenient here, taking into account all sorts of surprisingly-difficult math involving the Earth’s curvature.

Finally, the location object itself is added to a growing array of locations.

Note: The CLLocation object also contains information on altitude, with a corresponding verticalAccuracy value. As every runner knows, hills can be a game changer on any run, and altitude can affect the amount of oxygen available. A challenge to you, then, is to think of a way to incorporate this data into the app.

Saving the Run

At some point, despite that voice of motivation inside you that tells you to keep going (mine sounds like a high school coach), there comes a time to end the run. You already arranged for the UI to accept this input, and now it’s time to process that data.

Add this method to NewRunViewController.m:

- (void)saveRun
    Run *newRun = [NSEntityDescription insertNewObjectForEntityForName:@"Run" 
    newRun.distance = [NSNumber numberWithFloat:self.distance];
    newRun.duration = [NSNumber numberWithInt:self.seconds];
    newRun.timestamp = [NSDate date];
    NSMutableArray *locationArray = [NSMutableArray array];
    for (CLLocation *location in self.locations) {
        Location *locationObject = [NSEntityDescription insertNewObjectForEntityForName:@"Location"
        locationObject.timestamp = location.timestamp;
        locationObject.latitude = [NSNumber numberWithDouble:location.coordinate.latitude];
        locationObject.longitude = [NSNumber numberWithDouble:location.coordinate.longitude];
        [locationArray addObject:locationObject];
    newRun.locations = [NSOrderedSet orderedSetWithArray:locationArray]; = newRun;
    // Save the context.
    NSError *error = nil;
    if (![self.managedObjectContext save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

So what’s happening here? If you’ve done a simple Core Data flow before, this should look like a familiar way to save new objects. You create a new Run object, and give it the cumulative distance and duration values as well as assign it a timestamp.

Each of the CLLocation objects recorded during the run is trimmed down to a new Location object and saved. The locations link with the run, and then you’re good to go!

Finally, edit actionSheet:clickedButtonAtIndex: so that you stop reading locations and that you save the run before performing the segue. Find the method and make it look like this:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
    [self.locationManager stopUpdatingLocation];
    // save
    if (buttonIndex == 0) {
        [self saveRun]; ///< ADD THIS LINE
        [self performSegueWithIdentifier:detailSegueName sender:nil];
    // discard
    } else if (buttonIndex == 1) {
        [self.navigationController popToRootViewControllerAnimated:YES];

Send the Simulator On a Run

As much as I hope that this tutorial and the apps you build encourage more enthusiasm for fitness, the Build and Run phase does not need to be taken that literally while developing it.

You don’t need to lace up and head out the door either, for there’s a way to get the simulator to pretend it’s running!

Build & runin the simulator, and select Debug\Location\City Run to have the simulator start generating mock data:


Of course, this is much easier and less exhausting than taking a short run to testing this — or any other — location-based app.

However, I recommend eventually doing a true beta test with a device. Doing so gives you the chance to fine-tune the location manager parameters and assess the quality of location data you can really get.

Thorough testing could help instill a healthy habit, too. :]

Revealing the Map

Now it’s time to show the map post-run stats!

Open DetailViewController.m and add these imports to the top:

#import "MathController.h"
#import "Run.h"
#import "Location.h"

Then, make configureView look like this:

- (void)configureView
    self.distanceLabel.text = [MathController];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateStyle:NSDateFormatterMediumStyle];
    self.dateLabel.text = [formatter];
    self.timeLabel.text = [NSString stringWithFormat:@"Time: %@",  [MathController usingLongFormat:YES]];
    self.paceLabel.text = [NSString stringWithFormat:@"Pace: %@",  [MathController]];

This sets up the details of the run into the various labels.

Rendering the map will require just a little more detail. There are three basics steps to it. First, the region needs to be set so that only the run is shown and not the entire world! Then the line drawn over the top to indicate where the run went needs to be created and finally styled.

Add the following method:

- (MKCoordinateRegion)mapRegion
    MKCoordinateRegion region;
    Location *initialLoc =;
    float minLat = initialLoc.latitude.floatValue;
    float minLng = initialLoc.longitude.floatValue;
    float maxLat = initialLoc.latitude.floatValue;
    float maxLng = initialLoc.longitude.floatValue;
    for (Location *location in {
        if (location.latitude.floatValue < minLat) {
            minLat = location.latitude.floatValue;
        if (location.longitude.floatValue < minLng) {
            minLng = location.longitude.floatValue;
        if (location.latitude.floatValue > maxLat) {
            maxLat = location.latitude.floatValue;
        if (location.longitude.floatValue > maxLng) {
            maxLng = location.longitude.floatValue;
    } = (minLat + maxLat) / 2.0f; = (minLng + maxLng) / 2.0f;
    region.span.latitudeDelta = (maxLat - minLat) * 1.1f; // 10% padding
    region.span.longitudeDelta = (maxLng - minLng) * 1.1f; // 10% padding
    return region;

An MKCoordinateRegion represents the display region for the map, and you define it by supplying a center point and a span that defines horizontal and vertical ranges.

For example, my jog may be quite zoomed in around my short route, while my more athletic friend’s map will appear more zoomed out to cover all the distance she traveled. :]

It’s important to also account for a little padding, so that the route doesn’t crowd all the way to the edge of the map.

Next, add the following method:

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id < MKOverlay >)overlay
    if ([overlay isKindOfClass:[MKPolyline class]]) {
        MKPolyline *polyLine = (MKPolyline *)overlay;
        MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
        aRenderer.strokeColor = [UIColor blackColor];
        aRenderer.lineWidth = 3;
        return aRenderer;
    return nil;

This method says that whenever the map comes across a request to add an overlay, it should check if it’s a MKPolyline. If so, it should use a renderer that will make a black line. You’ll spice this up shortly. An overlay is something that is drawn on top of a map view. A polyline is such an overlay and represents a line drawn from a series of location points.

Lastly, you need to define the coordinates for the polyline. Add the following method:

- (MKPolyline *)polyLine {
    CLLocationCoordinate2D coords[];
    for (int i = 0; i <; i++) {
        Location *location = [ objectAtIndex:i];
        coords[i] = CLLocationCoordinate2DMake(location.latitude.doubleValue, location.longitude.doubleValue);
    return [MKPolyline polylineWithCoordinates:coords];

Here, you shoved the data from the Location objects into an array of CLLocationCoordinate2D, the required format for polylines.

Now, it’s time to put these three together! Add the following method:

- (void)loadMap
    if ( > 0) {
        self.mapView.hidden = NO;
        // set the map bounds
        [self.mapView setRegion:[self mapRegion]];
        // make the line(s!) on the map
        [self.mapView addOverlay:[self polyLine]];
    } else {
        // no locations were found!
        self.mapView.hidden = YES;
        UIAlertView *alertView = [[UIAlertView alloc]
                                  message:@"Sorry, this run has no locations saved."
        [alertView show];

Here, you make sure that there are points to draw, set the map region as defined earlier, and add the polyline overlay. Add the following code at the end of configureView:

[self loadMap];

And now build & run!. You should now see a map after your simulator is done with its workout.

notice how the default debug location just happens to be next-door to Apple :]

notice how the default debug location just happens to be next-door to Apple :]

Finding the Right Color

The app is pretty cool as-is, but one way you can help your users train even smarter is to show them how fast or slow they ran at each leg of the run. That way, they can identify areas where they are most at risk of straying from an even pace.

Click File\New\File. Select iOS\Cocoa Touch\Objective-C class. Call the file MulticolorPolylineSegment, that inherits from MKPolyline and create it. Then open MulticolorPolylineSegment.h and make it look like this:

#import <MapKit/MapKit.h>
@interface MulticolorPolylineSegment : MKPolyline
@property (strong, nonatomic) UIColor *color;

This special, custom, polyline will be used to render each segment of the run. The color is going to denote the speed and so the color of each segment is stored here on the polyline. Other than that, it’s the same as an MKPolyline. There will be one of these objects for each segment connecting two locations.

Next, you need to figure out how to assign the right color to the right polyline segment. That sounds like… math! Open MathController.h and add the following class method declaration:

+ (NSArray *)colorSegmentsForLocations:(NSArray *)locations;

Then open MathController.m and add the following imports at the top of the file:

#import "Location.h"
#import "MulticolorPolylineSegment.h"

Then add the following implementation of the method you just declared in the header:

+ (NSArray *)colorSegmentsForLocations:(NSArray *)locations
    // make array of all speeds, find slowest+fastest
    NSMutableArray *speeds = [NSMutableArray array];
    double slowestSpeed = DBL_MAX;
    double fastestSpeed = 0.0;
    for (int i = 1; i < locations.count; i++) {
        Location *firstLoc = [locations objectAtIndex:(i-1)];
        Location *secondLoc = [locations objectAtIndex:i];
        CLLocation *firstLocCL = [[CLLocation alloc] initWithLatitude:firstLoc.latitude.doubleValue longitude:firstLoc.longitude.doubleValue];
        CLLocation *secondLocCL = [[CLLocation alloc] initWithLatitude:secondLoc.latitude.doubleValue longitude:secondLoc.longitude.doubleValue];
        double distance = [secondLocCL distanceFromLocation:firstLocCL];
        double time = [secondLoc.timestamp timeIntervalSinceDate:firstLoc.timestamp];
        double speed = distance/time;
        slowestSpeed = speed < slowestSpeed ? speed : slowestSpeed;
        fastestSpeed = speed > fastestSpeed ? speed : fastestSpeed;
        [speeds addObject:@(speed)];
    return speeds;

This method returns the array of speed values for each sequential pair of locations.

The first thing you’ll notice is a loop through all the locations from the input. You have to convert each Location to a CLLocation so you can use distanceFromLocation:.

Remember basic physics: distance divided by time equals speed. Each location after the first is compared to the one before it, and by the end of the loop you have a complete collection of all the changes in speed throughout the run.

Next, add the following code, just before the return in the method you just added:

// now knowing the slowest+fastest, we can get mean too
double meanSpeed = (slowestSpeed + fastestSpeed)/2;
// RGB for red (slowest)
CGFloat r_red = 1.0f;
CGFloat r_green = 20/255.0f;
CGFloat r_blue = 44/255.0f;
// RGB for yellow (middle)
CGFloat y_red = 1.0f;
CGFloat y_green = 215/255.0f;
CGFloat y_blue = 0.0f;
// RGB for green (fastest)
CGFloat g_red = 0.0f;
CGFloat g_green = 146/255.0f;
CGFloat g_blue = 78/255.0f;

Here you define the three colors you’ll use for slow, medium and fast polyline segments.

Each color, in turn, has its own RGB components. The slowest components will be completely red, the middle will be yellow, and the fastest will be green. Everything else will be a blend of the two nearest colors, so the end result could be quite colorful.


And finally, remove the existing return and add the following to the end of the method:

NSMutableArray *colorSegments = [NSMutableArray array];
for (int i = 1; i < locations.count; i++) {
  Location *firstLoc = [locations objectAtIndex:(i-1)];
  Location *secondLoc = [locations objectAtIndex:i];
  CLLocationCoordinate2D coords[2];
  coords[0].latitude = firstLoc.latitude.doubleValue;
  coords[0].longitude = firstLoc.longitude.doubleValue;
  coords[1].latitude = secondLoc.latitude.doubleValue;
  coords[1].longitude = secondLoc.longitude.doubleValue;
  NSNumber *speed = [speeds objectAtIndex:(i-1)];
  UIColor *color = [UIColor blackColor];
  // between red and yellow
  if (speed.doubleValue < meanSpeed) {
    double ratio = (speed.doubleValue - slowestSpeed) / (meanSpeed - slowestSpeed);
    CGFloat red = r_red + ratio * (y_red - r_red);
    CGFloat green = r_green + ratio * (y_green - r_green);
    CGFloat blue = r_blue + ratio * (y_blue - r_blue);
    color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
    // between yellow and green
  } else {
    double ratio = (speed.doubleValue - meanSpeed) / (fastestSpeed - meanSpeed);
    CGFloat red = y_red + ratio * (g_red - y_red);
    CGFloat green = y_green + ratio * (g_green - y_green);
    CGFloat blue = y_blue + ratio * (g_blue - y_blue);
    color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
  MulticolorPolylineSegment *segment = [MulticolorPolylineSegment polylineWithCoordinates:coords count:2];
  segment.color = color;
  [colorSegments addObject:segment];
return colorSegments;

In this loop, you determine the value of each pre-calculated speed, relative to the full range of speeds. This ratio then determines the UIColor to apply to the segment.

Next, you construct a new MulticolorPolylineSegment with the two coordinates and the blended color.

Finally, you collect all the multicolored segments together, and you’re almost ready to render!

Applying the Multicolored Segments

Repurposing the detail view controller to use your new multicolor polyline is actually quite simple! Open DetailViewController.m and add the following import to the top of the file:

#import "MulticolorPolylineSegment.h"

Now, find loadMap:. Replace the following line:

[self.mapView addOverlay:[self polyLine]];


NSArray *colorSegmentArray = [MathController];
[self.mapView addOverlays:colorSegmentArray];

This creates the array of segments using the math controller and adds all the overlays to the map.

Lastly, you need to prepare your polyline renderer to pay attention to the specific color of each segment. So replace your current implementation of mapView:rendererForOverlay: with the following:

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id < MKOverlay >)overlay
    if ([overlay isKindOfClass:[MulticolorPolylineSegment class]]) {
        MulticolorPolylineSegment *polyLine = (MulticolorPolylineSegment *)overlay;
        MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
        aRenderer.strokeColor = polyLine.color;
        aRenderer.lineWidth = 3;
        return aRenderer;
    return nil;

This is very similar to what you had before, but now the specific color of each segment renders individually.

Alright! Now you’re all set to build & run, let the simulator go on a little jog, and check out the fancy multi-colored map afterward!


Leaving a Trail Of Breadcrumbs

That post-run map is stunning, but how about one during the run? The Breadcrumb sample project from Apple has this functionality, but as of the writing of this article, it uses some deprecated methods from pre-iOS 7.

Open Main.storyboard and find the ‘New Run’ view controller. Drag in a new MKMapView:


Then open NewRunViewController.m and add this import at the top:

#import <MapKit/MapKit.h>

And add the MKMapViewDelegate protocol conformation declaration to this line:

@interface NewRunViewController () <UIActionSheetDelegate, CLLocationManagerDelegate, MKMapViewDelegate>

Next, add an IBOutlet for the map to the class extension category:

@property (nonatomic, weak) IBOutlet MKMapView *mapView;

Then add this line to the end of viewWillAppear::

self.mapView.hidden = YES;

This makes sure that the map is hidden at first. Now add this line to the end of startPressed::

self.mapView.hidden = NO;

This makes the map appear when the run starts.

The trail is going to be another polyline, so it’s time to add your old friend, mapView:rendererForOverlay:. Add the following method:

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id < MKOverlay >)overlay
    if ([overlay isKindOfClass:[MKPolyline class]]) {
        MKPolyline *polyLine = (MKPolyline *)overlay;
        MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
        aRenderer.strokeColor = [UIColor blueColor];
        aRenderer.lineWidth = 3;
        return aRenderer;
    return nil;

This version is similar to the one for the run details screen, except that the strokeColor is always blue here.

Next, you need to write the code to update the map region and draw the polyline every time a valid location is found. Find your current implementation of locationManager:didUpdateLocations: and update it to this:

- (void)locationManager:(CLLocationManager *)manager
     didUpdateLocations:(NSArray *)locations
    for (CLLocation *newLocation in locations) {
        NSDate *eventDate = newLocation.timestamp;
        NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
        if (abs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20) {
            // update distance
            if (self.locations.count > 0) {
                self.distance += [newLocation distanceFromLocation:self.locations.lastObject];
                CLLocationCoordinate2D coords[2];
                coords[0] = ((CLLocation *)self.locations.lastObject).coordinate;
                coords[1] = newLocation.coordinate;
                MKCoordinateRegion region =
                MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 500, 500);
                [self.mapView setRegion:region animated:YES];
                [self.mapView addOverlay:[MKPolyline polylineWithCoordinates:coords count:2]];
            [self.locations addObject:newLocation];

Now, the map always centers on the most recent location, and constantly adds little blue polylines to show the user’s trail thus far.

Open Main.storyboard and find the ‘New Run’ view controller. Connect the outlet for mapView to the map view, and set the mapView’s delegate to the view controller.

Build & run, and start a new run. You’ll see the map updating in real-time!


Where To Go From Here

Great job! There are few cool ways to go from here:

  • Use the altitude information from the location updates in NewRunController to figure out how hilly the route is.
  • If you’re up for a pure-math challenge, try blending the segment colors more smoothly by averaging a segment’s speed with that of the segment before it.

Stay tuned for part two of the tutorial, where you’ll introduce a badge system personalized for each user.

As always, feel free to post comments and questions!

How To Make an App Like RunKeeper: Part 1 is a post from: Ray Wenderlich

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



Write a comment