怎么样制作一款像RunKeeper的应用:第二部分


原文地址:http://www.raywenderlich.com/74331/make-app-like-runkeeper-part-2
泰然翻译组:Stroustrup_Lee。校对:glroy。

这是教你制作一款像RunKeeper的应用的教程的第二部分,也是最后一部分的内容。这部分的内容主要完成地图着色和徽章的功能。

在教程的第一部分中,你完成了下面所列举的功能:

  • 使用Core Location记录你的路线
  • 不断的在地图上标记出你的路线,并实时的报告你的平均速度
  • 在跑步结束的时候显示出你的路线图;对地图路线进行着色,使慢的部分显示为红色,快的部分显示为绿色。
    • 这是一款非常优秀的记录数据和显示数据的应用,但是为了激发动力,我们的需求并不是仅仅的一个漂亮的地图就能满足的。

      因此,在这部分内容中,你将会完成一款有徽章系统的叫做MoonRunner 的应用,此应用更加的体现出健身是一项以娱乐和进步为基础的成就。下面就是其实现原理:

    • 一份标记着增加的距离检测点的列表来鼓励用户。
    • 在用户跑动的过程中,他们可以看到下一个徽章的缩略图和剩余距离。
    • 用户第一次达到一个距离检查点的时候,应用将奖励用户一个徽章并显示跑步的平均速度。
    • 在这之后,当用户以一个更快的速度达到这个检测点的时候,就会奖励银色或金色的徽章。
    • 地图上会以圆点的方式显示出路线上的每一个检测点,并允许用户用自定义的插图显示徽章的名称和图片。
    • 开始

      你可以在你徽章使用的主题方面随意的发挥你的创造性。但是我刚刚看完新版的电视剧:Cosmos ,因此我想把所有的徽章设计成星球的样子,如月亮或太阳系中的其他星球。那将是多么的浩瀚!

      下载MoonRunner的资源包并添加到你的项目中。注意,这个资源包包含很多的图片和一个名为badges.txt的文件。打开文件badges.txt,你会发现里面是一个很大的徽章对象的JSON数组。每一个徽章对象都有下面的属性:

    • 徽章的名称
    • 一段关于徽章的有趣的介绍
    • 取得该徽章的距离
    • 资源包中对应图片的文件名
    • 徽章都是从零开始计算--毕竟,我们总要有一个开头--到单程马拉松的距离。当然,有些人比超级马拉松走的还要远,那么你就可以认为这些有雄心的跑步的人已经进入了星际空间。

      因此,我们首先要把这个JSON文本解析成一个对象数组。选择File\New\FileiOS\Cocoa Touch\Objective-C class来创建一个继承自NSObjectBadge类。

      按照下面的内容编辑Badge.h:

      #import 
      
      
      
      @interface Badge : NSObject
      
      
      
      @property (strong, nonatomic) NSString *name;
      
      @property (strong, nonatomic) NSString *imageName;
      
      @property (strong, nonatomic) NSString *information;
      
      @property float distance;
      
      
      
      @end
      

      现在你拥有了Badge对象,是时候解析源JSON文件了。创建一个新的继承自NSObject 的类BadgeController,并按下面的方式编辑头文件:

      #import 
      
      
      
      @interface BadgeController : NSObject
      
      
      
      + (BadgeController *)defaultController;
      
      
      
      @end
      

      这个类被设计成单例模式,由defaultController创建和访问。打开文件BadgeController.m,并用下面的代码替换文件中的内容:

      #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
      

      这里共有三个协同工作的方法:

      1.defaultController 是一个public成员,生成控制器的实例,并确保解析操作只发生一次。

      2.badgeArray 提取文本文件中的数组,并为数组中的每一个元素创建一个对象。

      3.badgeForDictionary实现JSON关键字和Badge的实际映射。

      到目前为止,我们所做的全部是为了获取徽章数据。下面我们就把这些数据添加到我们的应用中去。

      徽章Storyboard

      如果不能把这些徽章保存在一个漂亮的玻璃箱内,可以在以后的生活中感慨这些成就的话,那这些徽章还有什么用处呢?或者是你非常谦虚的说只是想把他们记录下来。:)

      无论是哪种情况,现在都应该往你的Storyboard中添加可以查看徽章的界面了。打开 Main.storyboard,通过拖动往Storyboard中添加新的表试图控制器,然后按住Ctrl键和鼠标左键把主屏上的Badges按钮拖到一个新的视图控制器中来创建一个push类型的segue:

      然后,选择你刚刚添加的表视图控制器中的表视图并打开size inspector.把表的行高增加到80.然后选中表视图中的原型单元格(prototype cell)并打开属性查看器(attributes inspector).把类型设置成自定义,标示符设置成BadgeCell

      在单元格内,把黑色设置成MoonRunner的默认背景色,并在左侧添加一个大的UIImageView 。在右侧添加两个使用金色飞船和银色飞船的资源小一点的UIImageViews,然后添加两个UILabels。可以参考下面的截图来设计你的单元格:

      一个徽章占用一个单元格,并在左侧显示徽章的图片,在右侧显示你获得这枚徽章的时间或是为了获得这枚徽章你还需要做些什么。两个金色飞船和银色飞船的小UIImageViews 是在用户达到这个等级的时候给予的奖励。

      接下来,通过拖动往你的storyboard种添加一个新的视图控制器(这次不是表视图控制器)。然后在你刚刚添加的视图控制器中按住Ctrl键并拖动表视图单元格,选择push类型的segue。那么你的新屏幕看起来就和下面的类型类似了:

      仔细观察屏幕,你会发现:

      一个大的UIImageView 来显示徽章的图片--如果是太空中的照片就最好不过了

      在UIImageView的上面有一个小的UIButton ,使用info的图片作为背景图

    • 一个描述徽章名字的UILabel
    • 一个描述徽章代表的距离的UILabel
    • 一个记录获得徽章时间的UILabel
    • 一个记录这个距离的最快的平均速度
    • 一个记录用户获得这个徽章的银色徽章的日期(或是如果他们想获得银色徽章所需要的平均速度)
    • 一个记录用户获得这个徽章的金色徽章的日期(或是如果他们想获得金色徽章所需要的平均速度)
    • 两个分别用银色徽章和金色徽章的资源表示的UIImageViews
    • 徽章的信息是非常重要的,因为它以故事或者是图表的形式记录了用户取得这个成就的背景。无论多小的徽章图片都是一种额外的经历。

      赢取徽章

      你已经创建了徽章对象,现在你需要一个存放徽章获取时间的对象。这个对象通过各种Run对象和Badge对象联系到一起,如论用户获得哪种徽章。

      点击File\New\File。选择 iOS\Cocoa Touch\Objective-C类。调用继承自NSObject的BadgeEarnStatus类并保存文件。然后打开头文件BadgeEarnStatus.h,并用下面的内容替换其中的内容:

      #import 
      
      
      
      @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
      

      然后打开BadgeEarnStatus.m并在该文件的顶部添加下面的导入内容:

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

      既然你已经可以很简单的把Badge对象和Run对象联系到一起,下面就让我们来完成它们的连接逻辑。打开 BadgeController.h并在文件的顶部添加下面的常量:

      extern float const silverMultiplier;
      
      extern float const goldMultiplier;
      

      然后为接口添加下面的实例化函数:

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

      在其他的类中也可以访问常量 silverMultiplier和goldMultiplier 。它们是用户获取这些版本的徽章必须达到的速度值。

      打开BadgeController.m 并在文件的顶部添加下面的导入内容和常量定义:

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

      然后在实现部分添加下面的函数:

      - (NSArray *)earnStatusesForRuns:(NSArray *)runs {
      
      NSMutableArray *earnStatuses = [NSMutableArray array];
      
      
      
      for (Badge *badge in self.badges) {
      
      
      
      BadgeEarnStatus *earnStatus = [BadgeEarnStatus new];
      
      earnStatus.badge = badge;
      
      
      
      for (Run *run in runs) {
      
      
      
      if (run.distance.floatValue > badge.distance) {
      
      
      
      // 第一次获得该徽章
      
      if (!earnStatus.earnRun) {
      
      earnStatus.earnRun = run;
      
      }
      
      
      
      double earnRunSpeed = earnStatus.earnRun.distance.doubleValue / earnStatus.earnRun.duration.doubleValue;
      
      double runSpeed = run.distance.doubleValue / run.duration.doubleValue;
      
      
      
      // 是否可以获得银色徽章
      
      if (!earnStatus.silverRun
      
      && runSpeed > earnRunSpeed * silverMultiplier) {
      
      
      
      earnStatus.silverRun = run;
      
      }
      
      
      
      //是否可以获得金色徽章
      
      if (!earnStatus.goldRun
      
      && runSpeed > earnRunSpeed * goldMultiplier) {
      
      
      
      earnStatus.goldRun = run;
      
      }
      
      
      
      // 是否是该段距离的最好成绩
      
      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;
      
      }
      

      该函数把用户的所有跑步记录同每种徽章的距离要求作比较,建立它们之间的关系,并以数组的形式返回所有的BadgeEarnStatus 对象。

      用户第一次获得徽章的时候,会有一个earnRunSpeed的变量作为判断该用户是否有进步可以获得金色徽章或银色徽章的参考。

      按这样的方式处理的话,即使你的朋友每次都跑的特别的快,你也是有机会去赢取火星的金色徽章。只要你们两个都在提高,那么你就是赢家。

      注意:你会发现这个函数使用的是一段距离的整体的平均速度。如果想增加挑战的话,可以试着使用整个跑步过程中的一小段距离不断的计算速度。例如,你可能刚开始和快结束的时候跑的比较慢,但是中间两英里跑的特别的快。

      显示徽章

      现在,我们应该把徽章的计算逻辑和界面整合到一起呈现在用户的面前了。创建两个视图控制器和一个自定义的表单元使徽章的数据可以显示在storyboards上面。

      首先,创建一个继承自UITableViewCell的新类BadgeCell。打开BadgeCell.h并添加下面的内容:

      #import 
      
      
      
      @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
      

      现在你前面部分添加的徽章表试图控制器中拥有了一个可以使用的自定义的单元格。

      然后,创建一个继承自UITableViewController的类BadgesTableViewController。打开 BadgesTableViewController.h并添加下面的内容:

      #import 
      
      
      
      @interface BadgesTableViewController : UITableViewController
      
      
      
      @property (strong, nonatomic) NSArray *earnStatusArray;
      
      
      
      @end
      

      earnStatusArray中存放你在前面部分添加的计算徽章状态的函数earnStatusesForRuns的返回值。

      打开BadgesTableViewController.m并在该文件的顶部添加下面的导入内容:

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

      然后在该类的扩展类别中添加下面的属性:

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

      这些是在整个表视图控制器中使用的一部分属性。例如:其中颜色属性可以用来标示该徽章是否已经获得。

      在类的实现部分找到viewDidLoad ,并写成下面的样子:

      - (void)viewDidLoad
      
      {
      
      [super viewDidLoad];
      
      
      
      self.redColor = [UIColor colorWithRed:1.0f green:20/255.0 blue:44/255.0 alpha:1.0f];
      
      self.greenColor = [UIColor colorWithRed:0.0f green:146/255.0 blue:78/255.0 alpha:1.0f];
      
      self.dateFormatter = [[NSDateFormatter alloc] init];
      
      [self.dateFormatter setDateStyle:NSDateFormatterMediumStyle];
      
      self.transform = CGAffineTransformMakeRotation(M_PI/8);
      
      }
      

      该函数是用来设置你刚刚添加的属性的值的。这些属性实际上可以说是缓存,这样你就不用在每次创建一个新的单元格的时候重复的创建这些需要的属性。数据的格式化部分创建起来时非常的“昂贵的”(耗费时间和资源),因此最好的办法是创建缓存。

      然后,移除tableView:numberOfRowsInSectionnumberOfSectionsInTableView的实现部分,然后添加下面的函数:

      - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)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.png"];
      
      cell.userInteractionEnabled = NO;
      
      }
      
      
      
      return cell;
      
      }
      

      这些函数通知表视图应该显示多少行(徽章的数量)和怎么样设置每行。正如你所看到的,每一行的显示内容都取决于用户是否获得了该徽章。另外,改行只能在已经获取该徽章之后,通过使用userInteractionEnabled来选定。

      现在你需要往徽章的表视图控制器中添加一些数据。打开HomeViewController.m然后在文件的顶部添加下面的导入内容:

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

      在类的扩展类别中添加下面的属性:

      @property (strong, nonatomic) NSArray *runArray;
      

      现在添加下面的函数:

      - (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];
      
      }
      

      该函数会在每次显示视图控制器的时候刷新记录跑步数据的数组。该函数通过核心数据来获取按时间戳排序的所有的跑步数据来实现这个功能。

      最后,往prepareForSegue:sender添加下面的内容:

      - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
      
      {
      
      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];
      
      }
      
      }
      

      这里,当徽章的表视图控制器被添加到导航栈中时,所有徽章是否获取的状态都会被计算出来,然后传递给徽章的表视图控制器。

      现在是时候把storyboard中所有的内容联系起来了。打开 Main.storyboard并完成下面的内容:

    • 设置BadgeCell和BadgesTableViewController中的所有类
    • 把BadgeCell: nameLabel, descLabel, badgeImageView, silverImageView, 和goldImageView中所有的内容联系起来
    • 点击生成并运行,然后查看你的新徽章!你应该会看到类似下面的内容:

      你现在可能只有‘Earth’徽章,但是这仅仅是一个开始!下面就让我们去获取更多的徽章吧!

      用户要做些什么才能获得一个金色的徽章呢?

      MoonRunner的最后一个视图控制器是用来徽章的详细信息的。创建一个继承自UIViewController的类BadgeDetailsViewController。打开BadgeDetailsViewController.h 并用下面的内容替换原有的内容:

      #import 
      
      
      
      @class BadgeEarnStatus;
      
      
      
      @interface BadgeDetailsViewController : UIViewController
      
      
      
      @property (strong, nonatomic) BadgeEarnStatus *earnStatus;
      
      
      
      @end
      

      然后打开BadgesTableViewController.m。在该文件的顶部添加下面的导入内容:

      #import "BadgeDetailsViewController.h"
      

      并添加下面的函数:

      - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
      
      {
      
      if ([[segue destinationViewController] isKindOfClass:[BadgeDetailsViewController class]]) {
      
      NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
      
      BadgeEarnStatus *earnStatus = [self.earnStatusArray objectAtIndex:indexPath.row];
      
      [(BadgeDetailsViewController *)[segue destinationViewController] setEarnStatus:earnStatus];
      
      }
      
      }
      

      该函数在单元格被点击的时候设置segue。它把关于视图控制器详细信息的BadgeEarnStatus 相关实例显示出来。

      打开BadgeDetailsViewController.m,并在文件的顶部添加下面的导入内容:

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

      然后在类的扩展类别中添加下面的属性:

      @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
      

      这些都是界面相关的IBOutlets。

      现在找到viewDidLoad,并用下面的内容改变其原有内容:

      - (void)viewDidLoad
      
      {
      
      [super viewDidLoad];
      
      
      
      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]];
      
      }
      

      这段代码设置了徽章使用的图片并把赢取这些徽章的数据添加到标签中。

      最有趣的部分是那些告诉用户为了获得梦寐以求的银色或金色徽章所需要达到的速度的提示信息。我发现只要为了达到这个速度所需要付出的努力明显是可能的时候,这些提示是非常的有激励作用的。

      最后,添加下面的函数:

      - (IBAction)infoButtonPressed:(UIButton *)sender
      
      {
      
      UIAlertView *alertView = [[UIAlertView alloc]
      
        initWithTitle:self.earnStatus.badge.name
      
        message:self.earnStatus.badge.information
      
        delegate:nil
      
        cancelButtonTitle:@"OK"
      
        otherButtonTitles:nil];
      
      [alertView show];
      
      }
      

      当图片上面的info按钮被点击的时候就会调用上面的函数。它会在一个弹出窗口上面显示这个徽章的信息。

      好极了!!!现在徽章界面这方面的代码全部搞定了。打开Main.storyboard 并把下面的内容联系起来:

    • 设置类BadgeDetailsViewController
    • 把BadgeDetailsViewController: badgeImageView, bestLabel, distanceLabel, earnedLabel, goldImageView, goldLabel, nameLabel, silverImageLabel, 和silverLabel中的内容联系起来
    • BadgeDetailsView的接收动作infoButtonPressed
    • 点击生成并运行,查看你的新徽章的详细信息:

      "胡萝卜"激励法

      随着应用的徽章部分的完成,你需要检查和更新现有应用的界面来包含徽章系统!

      打开Main.storyboard并找到New Run视图控制器。在视图中的停止按钮的周围添加一个UIImageView 和一个UILabel 。大概是下面这样:

      这些就像是棍子上的胡萝卜,在用户跑步的过程中让他们可以看到下一个徽章以及他们到下一个徽章的距离。

      在你关联你的界面之前,你需要在BadgeController 中添加两个函数来确定一段距离最合适的徽章,和接下来显示哪一个徽章。

      打开BadgeController.h,在接口中添加下面两个函数声明:

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

      并在导入内容和接口之间添加下面一行内容:

      @class Badge;
      

      现在打开BadgeController.m并把函数的实现部分修改成下面这样:

      - (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 < badge.distance) {
      
      break;
      
      }
      
      }
      
      return nextBadge;
      
      }
      

      这些函数相当的直接--他们个有一个参数,传递以米为单位的距离,然后返回下面的内容:

    • bestBadgeForDistance:上一个获取的徽章
    • nextBadgeForDistance:下一个将要获取的徽章
    • 现在打开NewRunViewController.m并在文件的顶部添加下面的内容:

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

      和徽章相关的导入是必须的,导入的AudioToolbox 是为了在你每次获得新徽章的时候可以播放音效。

      在类的扩展类别中添加下面的三个属性:

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

      然后找到viewWillAppear并在函数的底部添加下面的代码:

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

      和其他的视图一样,在刚开始启动的时候徽章的标签和图片是需要隐藏起来的。

      然后找到startPressed并在函数的底部添加下面的代码:

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

      这是确保徽章的标签和图片在跑步开始的时候显示出来。

      现在找到eachSecond 并在函数的底部添加下面的代码:

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

      这是确保随着跑步的进行nextBadgeLabel一直最新的。

      然后,添加下面新的函数:

      - (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];
      
      }
      

      该函数通过你前面添加的函数获取下一个徽章。它通过和存放在upcomingBadge属性中的下一个将要获得的徽章的名字作比较来检查该徽章是不是最新的。如果两个名字不相同,那么就播放一个成功的音效来通知用户他获取了一个新的徽章!

      你可能发现了你还没有实现playSuccessSound。添加下面的函数:

      - (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);
      
      }
      

      该函数用来播放成功获取徽章的音效,同时还可以使用系统的震动ID使手机震动。它使手机震动主要是为了当用户在比较嘈杂的地方,如繁忙的街道;或者是他们在听着音乐而听不到获取徽章的音效的时候通知用户!

      打开Main.storyboard,找到New Run视图控制器。把nextBadgeLabel 的IBOutlets和nextBadgeImageView联系起来。然后点击生成并运行,并在你跑步的时候观察标签和图片的更新!当你获得新徽章的时候注意播放的音效。

      空间模式会使事情变的更好

      跑步结束之后,可以让用户查看刚刚跑步过程中获得徽章会是一个非常棒的功能。下面就让我们来添加这个功能!

      打开Main.storyboard并找到Run Details视图控制器。以和现存的MKMapView相同的框架添加一个UIImageView。然后在上面添加一个有着info图片的UIButton和一个有着解释标签的UISwitch 。界面看起来和下面差不多:

      通过在属性检查器中选择Hidden属性来隐藏你刚刚添加的图片视图。

      为能显示最新获得徽章和刚刚的跑步信息的空间模式添加一个开关将会使你的跑步信息变得更加的个性化。有没有感觉像完成一次跑步之后却发现自己跑到木星上去了。:)

      打开DetailViewController.m并在文件的顶部添加下面的导入内容:

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

      然后在类的扩展类别中添加下面的两个属性:

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

      然后在configureView的底部添加下面的代码:

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

      该函数用你刚刚获得徽章的图片来设置徽章的图片视图。它通过你在前面添加的函数获取上一个获得徽章来实现这个功能。

      现在添加下面的函数:

      - (IBAction)displayModeToggled:(UISwitch *)sender
      
      {
      
      self.badgeImageView.hidden = !sender.isOn;
      
      self.infoButton.hidden = !sender.isOn;
      
      self.mapView.hidden = sender.isOn;
      
      }
      

      当切换开关的时候将会触发该函数。开关被打开的时候,它将用图片切换掉地图,既是你在空间模式中了!

      最后,添加下面的函数:

      - (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];
      
      }
      

      当info按钮被按下的时候将会触发该函数。它显示徽章信息的警告。

      打开Main.storyboard,找到Run Details视图控制器。把badgeImageView, infoButton, displayModeToggled:, infoButtonPresse和你刚刚添加的视图联系起来。然后点击生成并运行,在艰难的长时间的跑步之后好好的享受获取的徽章的荣耀的吧!

      为你的小镇添加太阳系的映射

      地图可以帮你记录你的跑步路线,设置指出你在那一段区域的速度比较慢。将要添加的另一个游泳的功能是可以标记处你通过徽章检测点的确切时间,因此你就可以把跑步分成几段。

      Annotations是关于地图视图如何显示点数据的。下面是你需要的移动部分:

    • 一个采用MKAnnotation 来处理注释的数据部分的类。该类提供一个标示在地图上放置注释的坐标
    • 一个继承自MKAnnotationView 的类来管理把从MKAnnotation 传递过来的数据转换成可视的形式
    • 因此,你就从把徽章数据转换成符合MKAnnotation格式的对象的数组开始。然后就使用MKMapViewDelegate 中的函数 mapView:viewForAnnotation:把数据转换成MKAnnotationViews。

      创建一个继承自MKPointAnnotation的类BadgeAnnotation。然后打开BadgeAnnotation.h并用下面的代码替换其中的内容:

      #import 
      
      
      
      @interface BadgeAnnotation : MKPointAnnotation
      
      
      
      @property (strong, nonatomic) NSString *imageName;
      
      
      
      @end
      

      然后打开BadgeController.h ,在接口中添加下面的函数声明:

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

      在同一个文件中的导入内容的下面添加下面的一行内容:

      @class Run;
      

      接下来,打开BadgeController.m 并在文件的顶部添加下面的导入内容:

      #import 
      
      #import "Location.h"
      
      #import "MathController.h"
      
      #import "BadgeAnnotation.h"
      

      并在实现部分添加下面的函数:

      - (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 *firstLocCL = [[CLLocation alloc] initWithLatitude:firstLoc.latitude.doubleValue longitude:firstLoc.longitude.doubleValue];
      
      CLLocation *secondLocCL = [[CLLocation alloc] initWithLatitude:secondLoc.latitude.doubleValue longitude:secondLoc.longitude.doubleValue];
      
      
      
      distance += [secondLocCL distanceFromLocation:firstLocCL];
      
      locationIndex++;
      
      
      
      if (distance >= badge.distance) {
      
      BadgeAnnotation *annotation = [[BadgeAnnotation alloc] init];
      
      annotation.coordinate = secondLocCL.coordinate;
      
      annotation.title = badge.name;
      
      annotation.subtitle = [MathController stringifyDistance:badge.distance];
      
      annotation.imageName = badge.imageName;
      
      [annotations addObject:annotation];
      
      break;
      
      }
      
      }
      
      }
      
      
      
      return annotations;
      
      }
      

      该函数遍历跑步过程中的所有的位置并记录总距离。当这个总距离超过下一个徽章的检测值的时候,就创建一个BadgeAnnotation 。这个注释记录了徽章获取的坐标位置,徽章的名称,跑过的距离和徽章的图片的名字。

      现在数据已经准备好了!

      打开DetailViewController.m,并在文件的顶部添加下面的导入内容:

      #import "BadgeAnnotation.h"
      

      然后添加下面的函数:

      - (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;
      
      }
      

      这是MKMapViewDelegate 协议的一部分。每次在地图上显示给定注释的视图时该函数都会被调用。它的功能是返回一个视图,然后地图视图就会处理把该视图显示在地图上的正确位置的逻辑。

      在该函数中,你传进去一个注释作为参数告诉地图怎么样去渲染它。注意title和subtitle属性并不显示出来,它的渲染实在地图工具包中自动完成的。

      然后找到loopMap并在调用函数addOverlays的下面添加下面的这行代码:

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

      用上面的代码,你通知地图来显示注释。

      现在,你可以在跑过一段距离之后查看地图,你会发现所有的表示你通过的检测点的圆点。点击生成并运行这款应用,完成一段跑步并点击保存。地图就会有每一个获得徽章的注意。点击其中的一个,你就可以看到它的名称,图片和距离。漂亮吧!

      至此,这款应用就算完成了!祝贺你完成了这款应用的设计。

      发展方向

      通过这两部分指南的学习,你完成了这样的一款应用:

    • 使用 Core Location来测量和记录你的跑步过程
    • 显示实时的数据,如在动态的地图上显示跑步的平均速度,
    • 在地图上用有色的折线画出你的路线并在每一个检测点添加自定义的注释
    • 为用户在距离和速度上的提高授予徽章
    • 完善这款应用的一些想法:

    • 为用户过去的跑步机路添加一个表
    • 试着使用一小段的速度来获取徽章

    • 例如:如果你在一段10km的跑步过程中在中间的5km是最快的,那么给你一个5km的银色或金色徽章怎么样?
    • 计算两个检测点之间的平均速度并把它在MKAnnotationView 的标注中显示出来
    • 添加终生成就的徽章,例如:一周跑了100km或是一直跑1000km等等
    • 用服务器同步用户的跑步数据
    • 如果你想知道这款应用完成后的样子,你可以在这里下载整个项目的代码。

      感谢你阅读这篇指南!如果你有什么问题,评论或是想要分享你的成功,请在下面的评论区写出来!快乐的在太阳系中奔跑吧!:)

标签: iOS

?>