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;
@end
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;
@end
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;
@end
@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];
badge.name = [dictionary objectForKey:@"name"];
badge.information = [dictionary objectForKey:@"information"];
badge.imageName = [dictionary objectForKey:@"imageName"];
badge.distance = [[dictionary objectForKey:@"distance"] floatValue];
return badge;
}
@end
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 theBadge
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 theUIImageView
, using theinfo
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 theSilver
version of the badge - The same for the
Gold
version of the badge - Two small
UIImageView
whith thespaceship-silver
andspaceship-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;
@end
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;
@end
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;
@end
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;
@end
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 = earnStatus.badge.name;
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
andBadgesTableViewController
- Connect outlets of
BadgeCell:
nameLabel
,descLabel
,badgeImageView
,silverImageView
andgoldImageView
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;
@end
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;
@end
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.earnStatus.badge.name;
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]
initWithTitle:self.earnStatus.badge.name
message:self.earnStatus.badge.information
delegate:nil
cancelButtonTitle:@"OK"
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
andsilverLabel
- The received action
infoButtonPressed:
toBadgeDetailsView
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) {
break;
}
bestBadge = badge;
}
return bestBadge;
}
- (Badge *)nextBadgeForDistance:(float)distance {
Badge *nextBadge;
for (Badge *badge in self.badges) {
nextBadge = badge;
if (distance < nextBadge.distance) {
break;
}
}
return nextBadge;
}
bestBadgeForDistance:
The badge that was last wonnextBadgeForDistance:
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.upcomingBadge.name];
[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 &&
![nextBadge.name isEqualToString:self.upcomingBadge.name]) {
[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);
AudioServicesPlaySystemSound(soundID);
//also vibrate
AudioServicesPlaySystemSound(kSystemSoundID_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] bestBadgeForDistance:self.run.distance.floatValue];
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] bestBadgeForDistance:self.run.distance.floatValue];
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:badge.name
message:badge.information
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[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;
@end
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) {
break;
}
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];
locationIndex++;
if (distance >= badge.distance) {
BadgeAnnotation *annotation = [[BadgeAnnotation alloc] init];
annotation.coordinate = secondCL.coordinate;
annotation.title = badge.name;
annotation.subtitle = [MathController stringifyDistance:badge.distance];
annotation.imageName = badge.imageName;
[annotations addObject:annotation];
break;
}
}
}
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] annotationsForRun:self.run]];
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.