Kingyeung Chan


大家好,我系Monster.Chan,一名来自中国的 iOS / AWS / Unity3D 开发者,就职于SAINSTORE。在不断修炼,努力提升自己

结合工作经验,目前写了《Getting Started with AWS》、《Websites & Web Apps on AWS》,欢迎试读或者购买


This is the second and final part of the Run-Tracking tutorial.

In first part, you created an app that:

  • Use Core Location to track your route.
  • Continually maps your path and reports your average pace for your run.
  • Shows the map for yout route when the run is complete, the map line with mutilcolor to show your speed.

This app is great for recording and displaying data, but you may need a little more of a nudge than a pretty map can provide.

In this part, you'll complete the app with a badge system that embodies the concept that fitness is a fun and progress-based achievement. Here's how it works:

  • A list maps out checkpoints of increasing distance to motivate the user.
  • Seliver and gold versions of badge are awarded for reaching the checkpoint again at a proportionally fast speed.

Getting Started

I knowed you have downloadd Run_tracking resource in part one and added it to your project. Notice that pack contains a file named badges.txt which is a large JSON array of badge objects Each badge object contains:

  • A name
  • Information about the badge
  • The distance in meters to archive this bedge
  • The filename of the corresponding image in the resource pack

So the first task is parse the JSON text into an array of object.
Select File/New/File and the iOS/CocoaTouch/Objective-C class to create a class Badge that extens NSObject.

Then edit the Badge.h look like this:

#import <Foundation/Foundation.h>
@interface Badge : NSObject
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *imageName;
@property (strong, nonatomic) NSString *information;
@property float distance;

Now you have your badge object, and it's time to parse the source JSON. Create a new class BadgeController extending NSObject, and edit the header as follow:

#import <Foundation/Foundation.h>
@interface BadgeController : NSObject
+ (BadgeController *)defaultController;

This class will have a single instance, created once and accessed with defauleController. Open BadgeController.m. Replace the file contenst with the following code:

#import "BadgeController.h"
#import "Badge.h"
@interface BadgeController()
@property (strong, nonatomic) NSArray *badges;

@implementation BadgeController
+ (BadgeController *)defaultController { 
    static BadgeController *controller = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{ 
        controller = [[BadgeController alloc] init];
        controller.badges = [self badgeArray];
    return controller;
+ (NSArray *)badgeArray {
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"badges" ofType:@"txt"];
    NSString *jsonContent = [NSString stringWithContentsOfFile:filePath usedEncoding:nil error:nil];
    NSData *data = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
    NSArray *badgeDicts = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    NSMutableArray *badgeObjects = [NSMutableArray array];
    for (NSDictionary *badgeDict in badgeDicts) {
        [badgeObjects addObject:[self badgeForDictionary:badgeDict]];
    return badgeObjects;
+ (Badge *)badgeForDictionary:(NSDictionary *)dictionary {
    Badge *badge = [Badge new]; = [dictionary objectForKey:@"name"];
    badge.information = [dictionary objectForKey:@"information"];
    badge.imageName = [dictionary objectForKey:@"imageName"];
    badge.distance = [[dictionary objectForKey:@"distance"] floatValue];
    return badge;

There are three methods here that all work together.

  • defaultController is publicly accessible, delivers single instance of the controller, and make sure the parsing operation happens only once.

  • badgeArray extracts an array from the text file and creates an object from each element of the array.

  • badgeForDictionary mapping the JSON key to the Badge object.

The Badge StoryBoards

Open Main.storyboard, drag a new table view controller onto the storyboard and control-drag the Badge button on the Home screen to create push segue.

Next, select the table view inside that the table view controller you just added and open the size inspector. increase the Row Height of the table to 80 points. Then select to prototype cell in the table view and open the attributes inspector. Set the style to custom and identifier to BadgeCell.

Inside the cell, set the background with black color and add a large UIImageView on the left side. Add two small UIImageView with spaceship-gold and spaceship-silver assets just to its right. Then add two UILabel.

Each badge will occupy one of these cells, with its image on the left and a description of when you can earned the badge, or what you need to earn the badge.

Next, drag a new view controller (normal one, not table view controller this time) onto the storyboard. Then control-drag from the table view cell in the table view controller you just added to this new view controller, and select push selection segue.

Then make this view controller look like this:

On this view controller you'll see:

  • A large UIImageView to show off the badge image
  • A small UIButton on top of the UIImageView, using the info image as background
  • A UILabel for the badge name
  • A UILabel for the badge distance
  • A UILabel for date that the badge was earned
  • A UILabel for the best average pace for this distance
  • A UILabel for the date that the user earned the Silver version of the badge
  • The same for the Gold version of the badge
  • Two small UIImageView whith the spaceship-silver and spaceship-gold assets

Earning The Badge

Now, you need a object to store when the badge was earned. This object will associate a Badge with the various Run Object.

Click File\New\File. Select iOS\Cocoa Touch\Objective-C class. Call the calss BadgeEarnStatus, extending NSObject, and save the file. Then open BadgeReanStatus.h and replace its contens with the following:

#import <Foundation/Foundation.h>
@class Badge;
@class Run;
@interface BadgeEarnStatus : NSObject
@property (strong, nonatomic) Badge *badge;
@property (strong, nonatomic) Run *earnRun;
@property (strong, nonatomic) Run *silverRun;
@property (strong, nonatomic) Run *goldRun;
@property (strong, nonatomic) Run *bestRun;

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

#import "Badge.h"
#import "Run.h"

Now that you can associate a Badge with a Run. Open BadgeController.h and add the following constants at the top of the file:

extern float const silverMultiplier;
extern float const goldMultiplier;

Then add the following method to the intreface:

- (NSArray *)earnStatusesForRuns:(NSArray *)runArray;

Open BadgeController.m and add the following imports and constant definitions at the top of the file:

#import "BadgeEarnStatus.h"
#import "Run.h"
float const silverMultiplier = 1.05; // 5% speed increase
float const goldMultiplier = 1.10; // 10% speed increase

Then, add the following method to implementation:

- (NSArray *)earnStatusesForRuns:(NSArray *)runArray {
    NSMutableArray *earnStatuses = [NSMutableArray array];
    for (Badge *badge in self.badges) {
        BadgeEarnStatus *earnStatus = [BadgeEarnStatus new];
        earnStatus.badge = badge;

        for (Run *run in runArray) {

            if (run.distance.floatValue > badge.distance) {

                // this is when the badge was first earned
                if (!earnStatus.earnRun) {

                    earnStatus.earnRun = run;

                double earnRunSpeed = earnStatus.earnRun.distance.doubleValue / earnStatus.earnRun.duration.doubleValue;

                double runSpeed = run.distance.doubleValue / run.duration.doubleValue;

                // does it deserve silver?
                if (! earnStatus.silverRun && runSpeed > earnRunSpeed * silverMultiplier) {

                    earnStatus.silverRun = run;

                // does it deserve gold?
                if (! earnStatus.goldRun && runSpeed > earnRunSpeed * goldMultiplier) {

                    earnStatus.goldRun = run;

                // is it the best for this distance?
                if (! earnStatus.bestRun) {

                    earnStatus.bestRun = run;
                } else {

                    double bestRunSpeed = earnStatus.bestRun.distance.doubleValue / earnStatus.bestRun.duration.doubleValue;

                    if (runSpeed > bestRunSpeed) {

                        earnStatus.bestRun = run;

        [earnStatuses addObject:earnStatus];

    return earnStatuses;

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

Diaplayig The Badges

Now, it's time to bring the badge logic and UI together for the user. let's create two view controllers and one custom table cell in order to link the storyboard with the badge data.

First, create a new class called BadgeCell extending UITableViewCell. Open the BadgeCell.h make it look like this:

#import <UIKit/UIKit.h>

@interface BadgeCell : UITableViewCell

@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UILabel *descLabel;
@property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
@property (nonatomic, weak) IBOutlet UIImageView *silverImageView;
@property (nonatomic, weak) IBOutlet UIImageView *goldImageView;


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

Next, create a class called BadgeTableViewController extending UITableViewController. Open BadgeTableViewController.h and make it look like this:

#import <UIKit/UIKit.h>

@interface BadgesTableViewController : UITableViewController

@property (strong, nonatomic) NSArray *earnStatusArray;


The earnStatusArray will be the result of calling earnStatusesForRuns: in the BadgeController the method you have added earlier.

Open BadgeTableViewController.m add the following imports to the top of the file:

#import "BadgeEarnStatus.h"
#import "BadgeCell.h"
#import "Badge.h"
#import "MathController.h"
#import "Run.h"

Then add the following properties to the class extension category:

@interface BadgesTableViewController ()

@property (strong, nonatomic) UIColor *redColor;
@property (strong, nonatomic) UIColor *greenColor;
@property (strong, nonatomic) NSDateFormatter *dateFormatter;
@property (assign, nonatomic) CGAffineTransform transform;


There are a few properties that will used throughtout the table view controller.

Find ViewDidLoad: in the implementation and make it look like this:

- (void)viewDidLoad
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;

    self.redColor = [UIColor colorWithRed:1.0f green:20.0/255 blue:44.0/255 alpha:1.0];
    self.greenColor = [UIColor colorWithRed:0.0f green:146.0/255 blue:78.0/255 alpha:1.0];
    self.dateFormatter = [[NSDateFormatter alloc] init];
    self.transform = CGAffineTransformMakeRotation(M_PI / 8);

This set up the properties that you just added. 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.

Next, remove the implementation of the tableView:numberOfRowsInSection: and numberOfSectionsInTableView:. Then add the following method:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
#warning Potentially incomplete method implementation.
    // Return the number of sections.
    return 1;

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
#warning Incomplete method implementation.
    // Return the number of rows in the section.
    return self.earnStatusArray.count;

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

    BadgeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BadgeCell" forIndexPath:indexPath];
    BadgeEarnStatus *earnStatus = [self.earnStatusArray objectAtIndex:indexPath.row];

    cell.silverImageView.hidden = !earnStatus.silverRun;
    cell.goldImageView.hidden = !earnStatus.goldRun;

    if (earnStatus.earnRun) {

        cell.nameLabel.textColor = self.greenColor;
        cell.nameLabel.text =;

        cell.descLabel.textColor = self.greenColor;
        cell.descLabel.text = [NSString stringWithFormat:@"Earned: %@", [self.dateFormatter stringFromDate:earnStatus.earnRun.timestamp]];

        cell.badgeImageView.image = [UIImage imageNamed:earnStatus.badge.imageName];
        cell.silverImageView.transform = self.transform;
        cell.goldImageView.transform = self.transform;
        cell.userInteractionEnabled = YES;
    } else {

        cell.nameLabel.textColor = self.redColor;
        cell.nameLabel.text = @"?????";

        cell.descLabel.textColor = self.redColor;
        cell.descLabel.text = [NSString stringWithFormat:@"Run %@ to Earn",[MathController stringifyDistance:earnStatus.badge.distance]];

        cell.badgeImageView.image = [UIImage imageNamed:@"question_badge"];
        cell.userInteractionEnabled = NO;

    return cell;

These methods tell the table view how many rows to show and how to set up each row. By the way the cell onely can be selected if the badge has been earned, through the use of userInteractionEnabled.

Now you need to make the badges table view controller have some data to work with. Open HomeViewController.m and add these imports at the top of the file:

#import "BadgesTableViewController.h"
#import "BadgeController.h"

Then add this property to the class extension category:

@property (strong, nonatomic) NSArray *runArray;

Now add the following method:

- (void)viewWillAppear:(BOOL)animated {

    [super viewWillAppear:animated];

    NSFetchRequest *fetchrequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Run" inManagedObjectContext:self.managedObjectContext];
    [fetchrequest setEntity:entity];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:NO];
    [fetchrequest setSortDescriptors:@[sortDescriptor]];

    self.runArray = [self.managedObjectContext executeFetchRequest:fetchrequest error:nil];

This will have the effect of refreshing the run array every time the view controller appears. It does this using Core Data fetch to fetch all the runs sorted by timestamp.

Finally, add to prepareForSegue:sender: so it look like this:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.

    UIViewController *nextController = [segue destinationViewController];
    if ([nextController isKindOfClass:[NewRunViewController class]]) {

        ((NewRunViewController *) nextController).managedObjectContext = self.managedObjectContext;
    }else if ([nextController isKindOfClass:[BadgesTableViewController class]]) {

        ((BadgesTableViewController *) nextController).earnStatusArray = [[BadgeController defaultController] earnStatusesForRuns:self.runArray];

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

  • Set the class of BadgeCell and BadgesTableViewController
  • Connect outlets of BadgeCell: nameLabel, descLabel, badgeImageView, silverImageView and goldImageView

Build & Run and you can check out your new badges.

Show The Detail Data

The last view controller for this app is the one thas shows the detail of a badge. Create a new class named BadgeDetailViewController and extending from UIViewCOntroller. Open BadgeDetailViewController.h and replace its conents with the following:

#import <UIKit/UIKit.h>

@class BadgeEarnStatus;

@interface BadgeDetailsViewController : UIViewController

@property (strong, nonatomic) BadgeEarnStatus *earnStatus;


Then open BadgeDetailViewController.m. Add the following imports at the top of the file:

#import "BadgeEarnStatus.h"
#import "Badge.h"
#import "MathController.h"
#import "Run.h"
#import "BadgeController.h"

And add the following properties to the calss extension category:

@interface BadgeDetailsViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UILabel *distanceLabel;
@property (nonatomic, weak) IBOutlet UILabel *earnedLabel;
@property (nonatomic, weak) IBOutlet UILabel *silverLabel;
@property (nonatomic, weak) IBOutlet UILabel *goldLabel;
@property (nonatomic, weak) IBOutlet UILabel *bestLabel;
@property (nonatomic, weak) IBOutlet UIImageView *silverImageView;
@property (nonatomic, weak) IBOutlet UIImageView *goldImageView;


Now find the ViewDidLoad and make it look like this:

- (void)viewDidLoad
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateStyle:NSDateFormatterMediumStyle];

    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI / 8);

    self.nameLabel.text =;
    self.distanceLabel.text = [MathController stringifyDistance:self.earnStatus.badge.distance];
    self.badgeImageView.image = [UIImage imageNamed:self.earnStatus.badge.imageName];
    self.earnedLabel.text = [NSString stringWithFormat:@"Reached on %@", [formatter stringFromDate:self.earnStatus.earnRun.timestamp]];

    if (self.earnStatus.silverRun) {

        self.silverImageView.transform = transform;
        self.silverImageView.hidden = NO;
        self.silverLabel.text = [NSString stringWithFormat:@"Earned on %@", [formatter stringFromDate:self.earnStatus.silverRun.timestamp]];

    } else {

        self.silverImageView.hidden = YES;
        self.silverLabel.text = [NSString stringWithFormat:@"Pace < %@ for silver!", [MathController stringifyAvgPaceFromDist:(self.earnStatus.earnRun.distance.floatValue * silverMultiplier) overTime:self.earnStatus.earnRun.duration.intValue]];

    if (self.earnStatus.goldRun) {

        self.goldImageView.transform = transform;
        self.goldImageView.hidden = NO;
        self.goldLabel.text = [NSString stringWithFormat:@"Earned on %@", [formatter stringFromDate:self.earnStatus.goldRun.timestamp]];
    } else {

        self.goldImageView.hidden = YES;
        self.goldLabel.text = [NSString stringWithFormat:@"Pace < %@ for gold!", [MathController stringifyAvgPaceFromDist:(self.earnStatus.earnRun.distance.floatValue * goldMultiplier) overTime:self.earnStatus.earnRun.duration.intValue]];

    self.bestLabel.text = [NSString stringWithFormat:@"Best %@ %@", [MathController stringifyAvgPaceFromDist:self.earnStatus.bestRun.distance.floatValue overTime:self.earnStatus.bestRun.duration.intValue], [formatter stringFromDate:self.earnStatus.bestRun.timestamp]];

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

Finally, add the following method:

- (IBAction)infoButtonPressed:(id)sender {

    UIAlertView *alertView = [[UIAlertView alloc]
                              otherButtonTitles:nil, nil];

    [alertView show];

This will be invoked when the button is pressed. It shows a pop-up whith the badge's information.

Now you need to 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
  • The received action infoButtonPressed: to BadgeDetailsView

Now build & Run and check your new badge detail:

Carrot Movtivation

Along with you 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 New Run view controller. Add a UIImageView and UILabel to its view. It'll look like this:

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

Open BadgeController.h and add the following method:

- (Badge *)bestBadgeForDistance:(float)distance;
- (Badge *)nextBadgeForDistance:(float)distance;

Also add this line above the interface, just below the imports:

@class Badge;

Now open BadgeController.m and implement those methods like this:

- (Badge *)bestBadgeForDistance:(float)distance {

    Badge *bestBadge = self.badges.firstObject;
    for (Badge *badge in self.badges) {

        if (distance < badge.distance) {


        bestBadge = badge;

    return bestBadge;

- (Badge *)nextBadgeForDistance:(float)distance {

    Badge *nextBadge;
    for (Badge *badge in self.badges) {

        nextBadge = badge;
        if (distance < nextBadge.distance) {

    return nextBadge;
  • bestBadgeForDistance: The badge that was last won
  • nextBadgeForDistance: The badge that is next to be won

Now Open NewRunViewController.m and add the following imports at the top of the file:

#import <AudioToolbox/AudioToolbox.h>
#import "BadgeController.h"
#import "Badge.h"

And add three properties to the class extension category:

@property (nonatomic, strong) Badge *upcomingBadge;
@property (nonatomic, weak) IBOutlet UILabel *nextBadgeLabel;
@property (nonatomic, weak) IBOutlet UIImageView *nextBadgeImageView;

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

self.nextBadgeLabel.hidden = YES;
self.nextBadgeImageView.hidden = YES;

Then fine startPressed: and add following code at end of the method:

self.nextBadgeImageView.hidden = NO;
self.nextBadgeLabel.hidden = NO;

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

Now fine eachSecond and add the following code at end of the method:

self.nextBadgeLabel.text = [NSString stringWithFormat:@"%@ until %@!", [MathController stringifyDistance:(self.upcomingBadge.distance - self.distance)],];
[self checkNextBadge];

This make sure nextBadgeLabel is always up-to-date.

And then add this new method:

- (void)checkNextBadge {

    Badge *nextBadge = [[BadgeController defaultController] nextBadgeForDistance:self.distance];
    if (self.upcomingBadge &&
        ![]) {

        [self playSuccessSound];

    self.upcomingBadge = nextBadge;
    self.nextBadgeImageView.image = [UIImage imageNamed:nextBadge.imageName];

Maybe you have notice that you haven't implemented playSuccessSound. Let's add this method:

- (void)playSuccessSound {

    NSString *path = [NSString stringWithFormat:@"%@%@", [[NSBundle mainBundle] resourcePath], @"/success.wav"];
    SystemSoundID soundID;
    NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
    AudioServicesCreateSystemSoundID((CFURLRef)CFBridgingRetain(filePath), &soundID);

    //also vibrate

This plays the success sound, but it also vibrates the phone using the system vibrate sound ID. It also helps to vibrate the phone in case the user is running in a noisy location.

Open Main.storyboard and find New Run View Controller. Connect the IBOutlets for nextBadgeLabel and nextBadgeImageView Then Build & Run:

Add Space Mode

Open Main.storyboard and find Run Detail View Controller. Add a UIImageView with the same frame as the exiting MKMapView and set Hidden in the attributes inspector. Then add a UIButton with the info image on it, and a UISwitch with an explanatory UILabel above it. The UI should look like this:

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

#import "Badge.h"
#import "BadgeController.h"

Next, add two properties to the class extension category:

@property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
@property (nonatomic, weak) IBOutlet UIButton *infoButton;

Then add the following code to end of configureView:

Badge *badge = [[BadgeController defaultController]];
self.badgeImageView.image = [UIImage imageNamed:badge.imageName];

This set up the badge image view with the image for the badge that was last earned.

Now add the following method:

- (IBAction)displayModeToggled:(UISwitch *)sender {

    self.badgeImageView.hidden = !sender.isOn;
    self.infoButton.hidden = !sender.isOn;
    self.mapView.hidden = !sender.isOn;

This will be fired when the switch is toggled.

And finally, add the following method:

- (IBAction)infoButtonPressed {

    Badge *badge = [[BadgeController defaultController]];

    UIAlertView *alertView = [[UIAlertView alloc]
    [alertView show];

This will be fired when the info button is pressed.

Now open Main.storyboard and find the Detail View Controller. Connect badgeImageView, infoButton, displayModeToggled: and infoButtonPressed to the view you just added. Then Build & Run:

Mapping In Your Town

The post-run map alreadly helps you remember your route, and ever identify specific areas where your speed was lower. Another helpful feature that would be nice to add is to note when you pass each badge checkpoint, so you can divide up your run.

Annotations are how map view can display point like this.

So you'll begin by arranging the badge data into array of objects to conforming to MKAnnotation. Then you'll use the MKMapViewDelegate method mapView:viewForAnnotation: to translte that data into MKAnnotationViews.

Create a new class called BadgeAnnotation which extends MKPointAnnotation. Then open BadgeAnnotation.h and replace its contents with following code:

#import <MapKit/MapKit.h>

@interface BadgeAnnotation : MKPointAnnotation

@property (strong, nonatomic) NSString *imageName;


Then Open BadgeController.h and add this method declaration to the interface:

- (NSArray *)annotationsForRun:(Run *)run;

Add this line under the imports in the same file:

@class Run;

Next, open BadgeController.m and add the following imports to the top of the file:

#import "BadgeAnnotation.h"
#import "MathController.h"
#import "Location.h"
#import <MapKit/MapKit.h>

Then add the following method to the implementation:

- (NSArray *)annotationsForRun:(Run *)run {

    NSMutableArray *annotations = [NSMutableArray array];

    int locationIndex = 1;
    float distance = 0;

    for (Badge *badge in self.badges) {

        if (badge.distance > run.distance.floatValue) {


        while (locationIndex < run.locations.count) {

            Location *firstLoc = [run.locations objectAtIndex:locationIndex - 1];
            Location *secondLoc = [run.locations objectAtIndex:locationIndex];

            CLLocation *firstCL = [[CLLocation alloc] initWithLatitude:firstLoc.latitude.doubleValue longitude:firstLoc.longitude.doubleValue];
            CLLocation *secondCL = [[CLLocation alloc] initWithLatitude:secondLoc.latitude.doubleValue longitude:secondLoc.longitude.doubleValue];

            distance += [secondCL distanceFromLocation:firstCL];

            if (distance >= badge.distance) {

                BadgeAnnotation *annotation = [[BadgeAnnotation alloc] init];
                annotation.coordinate = secondCL.coordinate;
                annotation.title =;
                annotation.subtitle = [MathController stringifyDistance:badge.distance];
                annotation.imageName = badge.imageName;
                [annotations addObject:annotation];

    return annotations;

This method loops over all the location point in the run and keeps a cumulative distance for the run. When the cumulatview distance passes the next badge's threshold, a BadgeAnnotation is created.

Open DetailViewController.m and add this import to the top of the file:

#import "BadgeAnnotation.h"

Then add the following method:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {

    BadgeAnnotation *badgeAnnotation = (BadgeAnnotation *)annotation;

    MKAnnotationView *annView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"checkpoint"];
    if (! annView) {

        annView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"checkpoint"];
        annView.image = [UIImage imageNamed:@"mapPin"];
        annView.canShowCallout = YES;

    UIImageView *badgeImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 75, 50)];
    badgeImageView.image = [UIImage imageNamed:badgeAnnotation.imageName];
    badgeImageView.contentMode = UIViewContentModeScaleAspectFit;
    annView.leftCalloutAccessoryView = badgeImageView;

    return annView;

This is part of MKMapViewDelegate protocol.

Then find the loadMap and add the following line of code just underneath the call do addOverlays::

[self.mapView addAnnotations:[[BadgeController defaultController]]];

Now you can look at your map ater a run, and see all the dots that mean you passed a checkpoint. Build & Run the app, start and finish a run, and click Save. The map will now have annotations for each badge earned. Cilck one, and you can see its name, picture and distance.


Xcode Vector PDF

在iOS App展示效果中,图片一直是一个极其重要的元素。随着iPhone设备的迭代更新,屏幕在不断变大,分辨率在一直提高,在iPhone6、iPhone Plus发布前,但启动画面Default图片就需要@1x,@2x,568h@2x三个尺寸了,到了iPhone6,又多了一个尺寸,就是@3x图片。 为了保证App的j精致,为不同尺寸屏幕提供相应的图片时必不可少的,但作为开发者,如果遗忘了添加某个尺寸的图片将会严重影响App的展示效果,作为UI设计者,也要制作、切图这么多份,实在是增加不...…



在WWDC 2014会议,Apple Inc向我们展示了即将到来的HealthKit API和相应的Health App. 与此同时,相关Health and Fitness应用分类在App Store上越来越受欢迎。为什么Health App会变得这么受欢迎呢?从以下的的信息也许可以找到原因:* 健康是极其重要的* 智能手机可以作为管理和跟踪健康的辅佐工具* 有趣的应用利用徽章制度可以调动人的积极性,但最重要还是让你持续健身运动 这个章节会告诉你怎么实现一个Run-Tracking。你...…