/br&gt我们将见面好badge的一对。

安创建一个如RunKeeper一样的App(一)swift版

</br>
本博将未定期更新外网的iOS最新教程

简书: @西木

微博: @角落里之monster

本文翻译自raywenderlich,版权归原作者所有,转载请注明出处

原文地址为 http://www.raywenderlich.com/97944/make-app-like-runkeeper-swift-part-1

</br>

原博提供了两份示例代码,分别为刚开始项目配置时的、part 1完成时的,地址分别为:

http://cdn4.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Starter.zip

http://cdn4.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Part1-Final.zip

</br>
眼看篇教程将于你出示,如何做一个类于RunKeeper一样,基于GPS的足记下您走步轨迹的app.

这个新的app.我们虽叫它们MoonRunner吧.

连下去,你就要完成这动态轨迹记录app的保有力量

  • 追踪核心位置
  • 当你跑步时显示地图,并且实时更新行动路线
  • 以跑步时连连显示当前的平均速度
  • 冲距离不同设置徽章奖励系统
  • 当你这次跑步了的时刻显得这次跑步整体的周转轨迹

争创建一个诸如RunKeeper一样的APP(二)swift版

</br>
本博将无期更新外网的iOS最新教程

简书: @西木

微博: @角落里的monster

本文翻译自raywenderlich,版权归原作者所有,转载请注明出处

原文地址为 http://www.raywenderlich.com/97945/make-app-like-runkeeper-swift-part-2

</br>
这是亚有的,也是当下首教程的尾声一有,我们以见面好badge的一部分

以第一片受,我们完成了

  • 用Core Location记录轨道

  • 络绎不绝创新您的轨道又出示平均速度等

  • 飞步成功时展示跑步区域的地图,轨迹曲线也彩色显示,速度迟滞得有为红,速度快的片段吗绿色

本条App可以死好地亮和记录你的跑步数据,但是如果看而的奔走中显的各种变化,就不仅是一个地图可以表现得,还亟需做一些调整

当即同一有的被,你拿完成MoonRunner的奖系统之安装,它能够体现出你于倒过程遭到之赏心悦目和成就感。它会助您闹主动来采取App记录你走的过程

预备好解锁你第二统分额运动完了呢?开始吧

Getting Started

求下充斥本学科对应之代码,路径也:
http://cdn4.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Starter.zip

打开并运行程序就可以看见一个不行简短的布局:

  • 首页显示的是3只大概的领航按钮
  • 君得记下或开始同不行新的奔走记录之早晚所当的不胜界面是NewRun界面
  • 在一如既往蹩脚跑步的详情页可以看见这次跑步的详细信息,包括彩色标注的地图

Getting Started

设若您还从来不扣留罢第一局部的学科,可以查阅自己之前的博文

当项目文件之布局中早已包含了一个JSON文件,你可以预先翻转JSON文件,如果你惊叹的言辞

徽章系统会从0开始记录,首先你待完成一个遥远,当然,很多人唯恐会见做到还远之离,你为堪考虑是何等的力可以支撑她们成功这些

先是,我们若拿JSON数据易成为一个屡组,新建一个swift文件,命名吧Badge

接下来,用底的一对替换原文件之内容

import Foundation

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

class Badge {
  let name: String?
  let imageName: String?
  let information: String?
  let distance: Double?

  init(json: [String: String]) {
    name = json["name"]
    information = json["information"]
    imageName = json["imageName"]
    distance = (json["distance"] as NSString?)?.doubleValue
  }
}

若果字典中并没有含有的key的言辞,可以来以此问价查找

咱俩需要解析JSON数据全面而的徽章系统,仍然是是文件,创建一个像样命名为
BadgeController 并进入一下代码

class BadgeController {
  static let sharedController = BadgeController()

  lazy var badges : [Badge] = {
    var _badges = [Badge]()

    let filePath = NSBundle.mainBundle().pathForResource("badges", ofType: "json") as String!
    let jsonData = NSData.dataWithContentsOfMappedFile(filePath) as! NSData

    var error: NSError?
    if let jsonBadges = NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.AllowFragments, error: &error) as? [Dictionary<String, String>] {
      for jsonBadge in jsonBadges {
        _badges.append(Badge(json: jsonBadge))
      }
    }
    else {
      println(error)
    }

    return _badges
    }()

此间,你声明了 BadgeController 为一个单例,而且针对 badges
数组做了懒加载,当第一糟糕被调用的时刻,会透过Badges.json 来初始化

Starting the Run

第一,我们用针对项目举行有安装

  • 点击MoonRunner的project navigator
  • 选择Capabilities tab
  • 打开Background Modes
  • 勾选Location Updates

以此装置好保,即使你零时需要接听电话,程序推入后台时仍然保持位置信息之换代

图片 1

属下去,选择Info tab,打开Custom iOS Target
Properties,将下两履行在plist

key type value
NSLocationWhenInUseUsageDescription String MoonRunner wants to track your run
NSLocationAlwaysUsageDescription String MoonRunner wants to track your run

它们的用意是,iOS会弹有提示框询问用户是否同意该app使用location data

注意
只要您的app要上传App
Store,你得在App的discription中注明:在后台持续以GPS会回落电池的寿命

接下回去代码中,打开NewRunViewController.swift加入

import CoreLocation
import HealthKit

以你得广大的冲位置信息的API和Health模块的method

下一场,你还待以文书尾加入class
extension,来遵守CLLocationManagerDelegate协议

// MARK:- CLLocationManagerDelegate
extension NewRunViewController:CLLocationManagerDelegate{

}

跟着而待贯彻部分代理方,完成对位置信息更新的监听

连下去加入一些成员属性

var seconds = 0.0
var distance = 0.0

lazy var locationManager:CLLocationManager = {
    var _locationManager = CLLocationManager()

    _locationManager.deledate = self
    _locationManager.desiredAccuracy = kCLLocationAccuracyBest
    _locationManager.activityType = .Fitness

    // Movement threshold for new events
    _locationManager.distanceFilter = 10.0
    return _locationManager
}()

lazy var locations = [CLLocation]()
lazy var timer = NSTimer()

这些性之意

  • seconds : 记录轨道的事件间隔,单位是秒
  • distance : 截止当前天天跑了差不多远,单位是米
  • locationManager : 对象,在start或者stop的随时记录用户的职位
  • timer : 记录时刻,更新UI

Earning The Badge

君既创办了Badge, 那么现在若得一个对象来囤积你取得的徽章奖励

此目标急需把您的Badge 对象与Run
对象关联起来,如果有的话,还用恩能够存储这个徽章的级别

开辟 Badge.swift 在尾部添加以下代码

class BadgeEarnStatus {
  let badge: Badge
  var earnRun: Run?
  var silverRun: Run?
  var goldRun: Run?
  var bestRun: Run?

  init(badge: Badge) {
    self.badge = badge
  }
}

现若已经好拿 Badge 和 Run
联系起来了,那么我们就算需要树立它们中的逻辑关系

填补加以下代码到 Badge.swift 中

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

silverMultiplier 和
goldMultiplier是冲速度之进度来划分的,越多之加成会取得更胜似级别之嘉奖

然后,添加以下方式以 BadgeController 类中

func badgeEarnStatusesForRuns(runs: [Run]) -> [BadgeEarnStatus] {
    var badgeEarnStatuses = [BadgeEarnStatus]()

    for badge in badges {
      let badgeEarnStatus = BadgeEarnStatus(badge: badge)

      for run in runs {
        if run.distance.doubleValue > badge.distance {

          // This is when the badge was first earned
          if badgeEarnStatus.earnRun == nil {
            badgeEarnStatus.earnRun = run
          }

          let earnRunSpeed = badgeEarnStatus.earnRun!.distance.doubleValue / badgeEarnStatus.earnRun!.duration.doubleValue
          let runSpeed = run.distance.doubleValue / run.duration.doubleValue

          // Does it deserve silver?
          if badgeEarnStatus.silverRun == nil && runSpeed > earnRunSpeed * silverMultiplier {
            badgeEarnStatus.silverRun = run
          }

          // Does it deserve gold?
          if badgeEarnStatus.goldRun == nil && runSpeed > earnRunSpeed * goldMultiplier {
            badgeEarnStatus.goldRun = run
          }

          // Is it the best for this distance?
          if let bestRun = badgeEarnStatus.bestRun {
            let bestRunSpeed = bestRun.distance.doubleValue / bestRun.duration.doubleValue
            if runSpeed > bestRunSpeed {
              badgeEarnStatus.bestRun = run
            }
          }
          else {
            badgeEarnStatus.bestRun = run
          }
        }
      }

      badgeEarnStatuses.append(badgeEarnStatus)
    }

    return badgeEarnStatuses
  }

其一方法会吧用户之跑动距离与相应的褒奖的求举行只门当户对,返回一个屡屡组,数组里带有了所有的
BadgeEarnStatus

它的意向是,每当用户得到一个Badge的时,它见面出一个诸如对应之快,来判断这奖的级别是
silver version 还是 gold version

如,虽然您的同伴的进度比较你赶快,但是要是你的发展足够深得言,依然有会收获
gold version 的奖

CLLocationManager和它的部署

当懒加载的早晚,你尽管会见呢NewRunViewController设置代理CLLocationManagerDelegate

紧接着设置了精确性为best(_locationManager.desiredAccuracy =
kCLLocationAccuracyBest),在公走的下,你可以十分精确地念博好之职位信息,同样,你吧会见损耗比较多之电量

activityType属性设置为.Fitness的用:
例如当你过马路或者停止的时光,它会只能的变动配置崩你节省电量

distanceFilter设置为10米,相对于desireAccuracy,这个特性不会见潜移默化电量,它是便于显示其他属性的价值的

要你开个不大的测试你就是见面发现,你的各项移信息不是同一长条直线而是来无数锯齿状

高精度的distanceFilter就可减少锯齿,给您一个再度精确地轨道,但是,太胜之精度值会为您的轨道像素化(看到众多马赛克),所以10m凡一个对立比较适中的值

接下,在viewWillAppear(animated: BOOL)方法的末尾加上这无异履

locationManager.requestAlawayAuthorization()

斯点子是iOS8才有,用来求用户授权允许以位置信息,如果您想为您的App兼容iOS8前的本,还索要测试兼容性

联网下当实现中参加这个方法

override func viewWillDisappear(animated: Bool) {
   super.viewWillDisappear(animated)
   timer.invalidate()
}

此办法的意思是,当导航控制器不亮该页面时,时间的记录为会见告一段落

复添加是措施

func eachSecond(timer:NSTimer) {
        seconds++
        let secondsQuantity = HKQuantity(unit: HKUnit.secondUnit(), doubleValue: seconds)
        timeLabel.text = "Time: " + secondsQuantity.description
        let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: distance)
        distanceLabel.text = "Distance: " + distanceQuantity.description

        let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
        let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: seconds / distance)
        paceLabel.text = "Pace: " + paceQuantity.description
    }

以此方式各一样秒都见面调用,在调用的当儿,你所有的数据值都见面尾随时间变而创新

当您起来跑前为,还有最后一个主意而调用

func startLocationUpdates() {
        // Here, the location manager will be lazily instantiated
        locationManager.startUpdatingLocation()
    }

以此艺术会告诉manager,需要开更新位置信息了

当审奔跑之前也,还得在startPressed(sender:
AnyObject)方法吃投入这些代码

    seconds = 0.0
    distance = 0.0
    locations.removeAll(keepCapacity: false)
    timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "eachSecond", userInfo: nil, repeats: true)
    startLocationUpdates()

图片 2

是界面会连创新具有数据

编译运行,如果您start你就可以看见时间价值开始当相连加码

然而,distance和pace文本框不见面更新,因为您还无绘制位移轨迹,接下我们开就部分

Displaying the Badges

而今凡时刻向用户展示你持有的褒奖逻辑与UI界面了

君得创造两独控制器和一个自定义的 table cell 来展示 Badg e数据

创造一个新的swift文件命名为 BadgeCell

打开这个文件,用脚的代码替换原来的始末

import UIKit
import HealthKit

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

现在,你曾为此 table view controller 为 badges 自定义了一个cell

接下,在创建一个新的swift文件命名也 BadgesTableViewController,
打开文件替换里面的情节也

import UIKit
import HealthKit

class BadgesTableViewController: UITableViewController {
  var badgeEarnStatusesArray: [BadgeEarnStatus]!
  }

在前头的 BadgeController 里调用 badgeEarnStatusesForRuns(_:)
方法的时刻会回去一个 badgeEarnStatuesArray 数组

添加如下属性给刚刚之近乎

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

每个cell会根据奖章的不等来展示不同之颜料

这些属于性会保存于缓存里,不欲每次又创设,每次创建新的见面怪耗费性能,所以理应尽量考虑重复使用

下一场,给 UITableViewDataSource 添加如下实现

// MARK: - UITableViewDataSource
extension BadgesTableViewController {
  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return badgeEarnStatusesArray.count
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("BadgeCell") as! BadgeCell

    let badgeEarnStatus = badgeEarnStatusesArray[indexPath.row]

    cell.silverImageView.hidden = (badgeEarnStatus.silverRun != nil)
    cell.goldImageView.hidden = (badgeEarnStatus.goldRun != nil)

    if let earnRun = badgeEarnStatus.earnRun {
      cell.nameLabel.textColor = greenColor
      cell.nameLabel.text = badgeEarnStatus.badge.name!
      cell.descLabel.textColor = greenColor
      cell.descLabel.text = "Earned: " + dateFormatter.stringFromDate(earnRun.timestamp)
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.silverImageView.transform = transform
      cell.goldImageView.transform = transform
      cell.userInteractionEnabled = true
    }
    else {
      cell.nameLabel.textColor = redColor
      cell.nameLabel.text = "?????"
      cell.descLabel.textColor = redColor
      let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
      cell.descLabel.text = "Run \(distanceQuantity.description) to earn"
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.userInteractionEnabled = false
    }

    return cell
  }
}

夫法告诉 tableView
要显示小行,每个cell显示什么内容,你能看出,每个cell对应的凡殊的badge,而且,因为安装了
userInteractionEnabled,只有取得奖章的 cell才能够让入选

今昔公需要被 BadgesTableViewController 提供部分数额,打开
HomeViewController.swift 给 prepareForSegue(_:sender): 方法添加如下代码

else if segue.destinationViewController.isKindOfClass(BadgesTableViewController) {
      let fetchRequest = NSFetchRequest(entityName: "Run")

      let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false)
      fetchRequest.sortDescriptors = [sortDescriptor]

      let runs = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as! [Run]

      let badgesTableViewController = segue.destinationViewController as! BadgesTableViewController
      badgesTableViewController.badgeEarnStatusesArray = BadgeController.sharedController.badgeEarnStatusesForRuns(runs)
    }

此间,当 BadgesTableViewController
被压入导航栈里的当儿,每一个嘉奖的状态且见面受算以出示出

链接storyboard,打开Main.storyboard做下的事情

  • 绑定 BadgeCell 和 BadgesTableViewController
  • 脱线设置 name标签、Earned标签、头如icon和奖赏标识如图所示

图片 3

若是你曾为此了她来跑吧,肯定已经获了 earth
级别之嘉奖,显然,奖励才刚刚起

Recording the Run

你已经创造了一个CLLocationManager对象,你应当去那里将到要创新的数码,这些可以透过代办来落实

还是NewRunViewController.swift这个文件,给咱们事先写的 class extension
实现 CLLocationManagerDelegate 代理方

func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
        for location in locations as! [CLLocation] {
            if location.horizontalAccuracy < 20 {
                // update distance
                if self.locations.count > 0 {
                    distance += location.distanceFromLocation(self.locations.last)
                }

                //sace location
                self.locations.append(location)
            }
        }
    }

一经有岗位更新的当儿这方法就是见面让调用,通常状况下,locations这个数组只生一个因素,如果有多单,它们会照时间先后排序

CLLocation中含了很多信息,包括经纬度等等。但是以读取这些消息之前,会发生一个horizonAccuracy的稽核,如果设备看这项数据以20米中的价值不是很可靠之上,会活动的以这项数据从数量集中移除。这个对功能在跑的时段极其重要,用户在率先浅开行开展校准的时节,这个时段,它恐怕会见更新一些勿规范的多少

而CLLocation通过了检测,就会开始计算距离。这时候distaFromLocation(_location:
CLLocation)方法就挺有益于了,它会考虑到各种奇怪的关联到地曲面的情

最终,添加这个位置对象特别成一个一段时间内位产生的盈盈多独职务对象的数组

注意
CLLocation对象为饱含了彼此对应之VerticalAccuracy的海拔的数值,每一个runner都清楚,小山坡会于跑步过程添加很多免平等的感想,因为海拔高度会潜移默化您对氧的需求量,它见面被你有小小的挑战,当然,这个数额吧会引用在App里

Badge Details

产一个决定器用来展示奖的详细信息

创造一个初的swift文件命名为 BadgeDetailsViewController 并且替换内容为

import UIKit
import HealthKit

class BadgeDetailsViewController: UIViewController {
  var badgeEarnStatus: BadgeEarnStatus!

  @IBOutlet weak var badgeImageView: UIImageView!
  @IBOutlet weak var silverImageView: UIImageView!
  @IBOutlet weak var goldImageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var distanceLabel: UILabel!
  @IBOutlet weak var earnedLabel: UILabel!
  @IBOutlet weak var silverLabel: UILabel!
  @IBOutlet weak var goldLabel: UILabel!
  @IBOutlet weak var bestLabel: UILabel!
  }

这个类用来存储你的获奖的详细状态,可以据此来填补加标识

增长如下的代码设置View

override func viewDidLoad() {
    super.viewDidLoad()

    let formatter = NSDateFormatter()
    formatter.dateStyle = .MediumStyle

    let transform = CGAffineTransformMakeRotation(CGFloat(M_PI/8.0))

    nameLabel.text = badgeEarnStatus.badge.name

    let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
    distanceLabel.text = distanceQuantity.description
    badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)

    if let run = badgeEarnStatus.earnRun {
      earnedLabel.text = "Reached on " + formatter.stringFromDate(run.timestamp)
    }

    if let silverRun = badgeEarnStatus.silverRun {
      silverImageView.transform = transform
      silverImageView.hidden = false
      silverLabel.text = "Earned on " + formatter.stringFromDate(silverRun.timestamp)
    }
    else {
      silverImageView.hidden = true
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
      silverLabel.text = "Pace < \(paceQuantity.description) for silver!"
    }

    if let goldRun = badgeEarnStatus.goldRun {
      goldImageView.transform = transform
      goldImageView.hidden = false
      goldLabel.text = "Earned on " + formatter.stringFromDate(goldRun.timestamp)
    }
    else {
      goldImageView.hidden = true
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
      goldLabel.text = "Pace < \(paceQuantity.description) for gold!"
    }

    if let bestRun = badgeEarnStatus.bestRun {
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: bestRun.duration.doubleValue / bestRun.distance.doubleValue)
      bestLabel.text = "Best: \(paceQuantity.description), \(formatter.stringFromDate(bestRun.timestamp))"
    }
  }

立段代码设置了 badge image和血脉相通的label中的多少

最为有趣的组成部分是鞭策用户怎么样获得重新胜似级别的奖励,这些鼓励会增加而的积极向上,因为其要再快地跑记录

最后,添加这个点子

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

当用户点击info按钮的早晚会过来此处,将会显示badge的音信

本详情页设置结束了,你还需要保证在segue之前badges table
view能够发送badge信息

开拓BadgesTableViewController.swift 给
BadgesTableViewController添加如下方法

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

当cell被点击的时候,BadgesDetailsViewController能展示相关的BadgeEarnStatus

现在,UI部分设置完了,打开Main.storyb做而下链接

  • 绑定BadgeDetailsViewController
  • 为BadgeDetailsVireController设置badgeImageView,bestLabel,distanceLabel,earnedLabel,goldImageView,goldLabel,nameLabel,silverImageLabel,silverLabel
  • 为info按钮设置点击事件

图片 4

Send the Simulator on a run

自家愿意以此课程以及App的开放能够让你对运动以及健身产生巨大的古道热肠,但是你开的时段不需按字面意思逐字地失去了解她

乃无待以测试的时刻的确的以在手机去奔,模拟器就可辅助你成功这个职责

于模拟器中启动程序,然后择Debug ->Location ->City
Run,模拟器就会给你一个虚构的多寡

图片 5

当,这样比较易于吗不需耗费精力去测试相对于外的基于位置信息的App

不过,我为会提议您实在拿在手机召开一个无疑测试,这样您才生机遇错过微调你的岗位管理的参数,去评估你取的数质量

再者为推动你养成健康之生活习惯

Badge Motivation

作为徽章奖励系统新的同片,你得返回UI部分,把其纳入之前的徽章体系中

开辟Main.storyboard,找到new
Run场景,在stop按钮的顶端添加一个UIImageView和一个UILabel

为UIImageView,使用机关布局设置约

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

也UILabel,使用自动布局设置约

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

乍的界面长长这样

图片 6

初的view在start按钮的地方会面产生局部重叠,但是在开跑后start按钮会隐藏起来才显示另外两独控件

以跑时见面下“carrot-on-a-stick”方式激励用户,会显一个岩的规范来描述您距离下一个级别之奖还有都丢掉差距

以显示UI之前,你待补加少个章程被 BadgeController
来决定你无比好以以这次可以以到某奖励然后再度下同样不成就是好拿到另外一个记功

打开 Badge.swift 给 BadgeController添加以下方法

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

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

这老简短,只要您输入距离,就会回去

  • bestBadgeForDistance(_:): 你眼前亦可取的赏
  • nextBadgeForDistance(_:): 你生一个可知收获的奖赏

开辟NewRunViewController.swift在顶部导入

import AudioToolbox

导入AudioToolbox之后您就算可知在用户每次得到新奖励的时段播放音效

搭下去,为NewRunViewController 添加以下属性

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

在viewWillAppear(_:)方法结尾处添加

nextBadgeLabel.hidden = true
nextBadgeImageView.hidden = true

badge label和badge image 一开始是得隐藏的

给 startPressed(_:)方法结尾处添加

nextBadgeLabel.hidden = false
nextBadgeImageView.hidden = false

于 badge label 和 badge image 在跑步开始后显示

加上下两单道

func playSuccessSound() {
    let soundURL = NSBundle.mainBundle().URLForResource("success", withExtension: "wav")
    var soundID : SystemSoundID = 0
    AudioServicesCreateSystemSoundID(soundURL, &soundID)
    AudioServicesPlaySystemSound(soundID)

    //also vibrate
    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate));
  }

  func checkNextBadge() {
    let nextBadge = BadgeController.sharedController.nextBadgeForDistance(distance)

    if let upcomingBadge = upcomingBadge {
      if upcomingBadge.name! != nextBadge.name! {
        playSuccessSound()
      }
    }

    upcomingBadge = nextBadge
  }

率先只艺术播放音效的下啊会见生出震动,以便在喧嚣的环境里通报用户要防范播放音乐之进程被无法听到音效

当用户满足获一个赏的标准化的时光会调用第二独道检测这次获得的奖是免是达标一致不好获奖时记下之产一致不良将得到的生奖,如果是,允许播放音效,并且将生一样不善将要取得的褒奖保存下去

为 eachSecond(_:)方法添加

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

随即段代码可以让 nextBadgeLabel 和 nextBadgeImageView
在跑步的过程遭到频频更新

编译运行,start a new run

若得看label和image在不断更新

图片 7

Saving the Run

乃之前已经计划好了UI,那么现在装数据吧

进入这个方式及NewRunViewController.swift中

func saveRun() {
    // 1
    let savedRun = NSEntityDescription.insertNewObjectForEntityForName("Run",
      inManagedObjectContext: managedObjectContext!) as! Run
    savedRun.distance = distance
    savedRun.duration = seconds
    savedRun.timestamp = NSDate()

    // 2
    var savedLocations = [Location]()
    for location in locations {
      let savedLocation = NSEntityDescription.insertNewObjectForEntityForName("Location",
        inManagedObjectContext: managedObjectContext!) as! Location
      savedLocation.timestamp = location.timestamp
      savedLocation.latitude = location.coordinate.latitude
      savedLocation.longitude = location.coordinate.longitude
      savedLocations.append(savedLocation)
    }

    savedRun.locations = NSOrderedSet(array: savedLocations)
    run = savedRun

    // 3
    var error: NSError?
    let success = managedObjectContext!.save(&error)
    if !success {
      println("Could not save the run!")
    }
  }

此处开了呀也?如果您前面举行了Core Data
Flow的语,这看起特别像是保存了同样久新的奔跑记录:

1.君创造了一个初的笔录,记录了你运动的相距和花的岁月

2.当把同层层之CLLocation对象保存到一个Location对象中的下,你尽管拿跑步经过的那些坐标点都连接起来了

3.保存NSManagedObjectContext

终极,当用户已奔跑又只要管这次跑步的笔录保留下来的早晚即便会见调用这个法子。找到

extension NewRunViewController: UIActionSheetDelegate {
}

这个extension,在

if buttonIndex == 1

此block里地第一履写上

saceRun()

编译、运行,你便好开同不行新的跑动记录同时将数量保存下去

图片 8

但是,跑步详细信息的界面依然是空的,现在我们去就她

Where to go From Here

恭喜你!

成就了一个得以跑过程被实时记录运行轨道又产生好激励体系的App

乃可以当这里下载完整代码

http://cdn3.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Part2-Final.zip

依据当时半首教程,你开了一个app

  • 之所以Core Location 测量你的轨道
  • 著跑步过程遭到之实时数据
  • 每当地形图及之所以不同颜色之曲线来号你的轨道和职务
  • 进度跟距离的私奖励系统

这app只是瓜熟蒂落了当下类app的根底力量,要于与多的食指用你的app你还亟需做更多地全盘,成就奖励是一个“游戏化”app很好的法

要你想吃您的app有愈来愈的升迁,你还索要开

  • 来得用户的跑动历史记录
  • 以嘉奖与速值标注在轨道及
  • 拿奖励与注释标注在地形图及

Revealing the Map

现行,需要我们去调出地图,打开 DetailViewController.swift 并且导入
Healthkit

import HralthKit

下一场,将下的代码写到configView()方法里

func configureView() {
    let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: run.distance.doubleValue)
    distanceLabel.text = "Distance: " + distanceQuantity.description

    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = .MediumStyle
    dateLabel.text = dateFormatter.stringFromDate(run.timestamp)

    let secondsQuantity = HKQuantity(unit: HKUnit.secondUnit(), doubleValue: run.duration.doubleValue)
    timeLabel.text = "Time: " + secondsQuantity.description

    let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
    let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: run.duration.doubleValue / run.distance.doubleValue)
    paceLabel.text = "Pace: " + paceQuantity.description

  }

此装了驱的详细信息,可以来得在屏幕的文本框里

1.首先,要装你所当的地理位置

2.安了动轨迹的起始点

3.装了速度的显示风格

用下面的法子在类吃

func mapRegion() -> MKCoordinateRegion {
    let initialLoc = run.locations.firstObject as! Location

    var minLat = initialLoc.latitude.doubleValue
    var minLng = initialLoc.longitude.doubleValue
    var maxLat = minLat
    var maxLng = minLng

    let locations = run.locations.array as! [Location]

    for location in locations {
      minLat = min(minLat, location.latitude.doubleValue)
      minLng = min(minLng, location.longitude.doubleValue)
      maxLat = max(maxLat, location.latitude.doubleValue)
      maxLng = max(maxLng, location.longitude.doubleValue)
    }

    return MKCoordinateRegion(
      center: CLLocationCoordinate2D(latitude: (minLat + maxLat)/2,
        longitude: (minLng + maxLng)/2),
      span: MKCoordinateSpan(latitudeDelta: (maxLat - minLat)*1.1,
        longitudeDelta: (maxLng - minLng)*1.1))
  }

MKCoordinateRegion可以依据你提供的着力位置,水平和竖直范围来确定当前屏幕显示的凡谁区域

比如,当您想吃你的活动轨迹的亮看起较舒畅一点的话,可以拖拽或者缩放地图。这或多或少急需明白地报告用户,这样他看到底途径才会显示在屏幕中心

继而,添加底下是办法

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
    if !overlay.isKindOfClass(MKPolyline) {
      return nil
    }

    let polyline = overlay as! MKPolyline
    let renderer = MKPolylineRenderer(polyline:polyline)
    renderer.strokeColor = UIColor.blackColor()
    renderer.lineWidth = 3
    return renderer
  }

是法表示当代表轨迹的曲线重合的时节,你的轨道曲线的颜色会火上浇油,让您看起再直观,颜色加深的那么有些凡由一系列的位置点产生的

联网下,你用也polyline定义一个coordinates,添加这个主意

func polyline() -> MKPolyline {
    var coords = [CLLocationCoordinate2D]()

    let locations = run.locations.array as! [Location]
    for location in locations {
      coords.append(CLLocationCoordinate2D(latitude: location.latitude.doubleValue,
        longitude: location.longitude.doubleValue))
    }

    return MKPolyline(coordinates: &coords, count: run.locations.count)
  }

此而用Location的多寡传到了CLLocationCoordinate2D这个数组中

接下来,添加以下方法

func loadMap() {
    if run.locations.count > 0 {
      mapView.hidden = false

      // Set the map bounds
      mapView.region = mapRegion()

      // Make the line(s!) on the map
      loadMap()
    } else {
      // No locations were found!
      mapView.hidden = true

      UIAlertView(title: "Error",
        message: "Sorry, this run has no locations saved",
        delegate:nil,
        cancelButtonTitle: "OK").show()
    }
  }

这个法子被,位置点绘制了,地图显示的区域也始发跑前安装的区域,重合轨迹的一些做了火上浇油的颜色渲染

说到底,将是措施上加至configView()的末尾

loadMap()

今昔,编译运行,你就是得当模拟器上见到这般的地形图显示了

图片 9

end

Finding the Right Color

是App已经特别cool了,但是你还得展示用户跑的究竟发生多快,那样,他们就是可分辨在不同之地形条件被,他们生没起保持以适宜的速率达到

倘召开这效果的话,你待扩大polyline这个类似

新建一个像样,叫做MulticolorPolylineSegment,打开,删除里面的内容,写副以下代码

import UIKit
import MapKit

class MulticolorPolylineSegment: MKPolyline {
  var color: UIColor?
 }

本条自定义的polyline将因此来渲染轨迹的各一个片。颜色之深浅将代表速度的快,如此以外,它跟MKPolyline是同样的。它们还是因此来形容连接两只位置点支架的线

连片下你要规定,在哪些的线上面运用什么的颜料。添加这个类似措施在MulticolorPolylineSegment
这个看似中

private class func allSpeeds(forLocations locations: [Location]) -> (speeds: [Double], minSpeed: Double, maxSpeed: Double) {
    // Make Array of all speeds. Find slowest and fastest
    var speeds = [Double]()
    var minSpeed = DBL_MAX
    var maxSpeed = 0.0

    for i in 1..<locations.count {
      let l1 = locations[i-1]
      let l2 = locations[i]

      let cl1 = CLLocation(latitude: l1.latitude.doubleValue, longitude: l1.longitude.doubleValue)
      let cl2 = CLLocation(latitude: l2.latitude.doubleValue, longitude: l2.longitude.doubleValue)

      let distance = cl2.distanceFromLocation(cl1)
      let time = l2.timestamp.timeIntervalSinceDate(l1.timestamp)
      let speed = distance/time

      minSpeed = min(minSpeed, speed)
      maxSpeed = max(maxSpeed, speed)

      speeds.append(speed)
    }

    return (speeds, minSpeed, maxSpeed)
  }

是方法会返回一个屡屡组,这个累组装得是多元的职位点相对应的速值,其中为就是连了无与伦比充分快及最好小速度。返回的大都个价,你得将它放在一个元组里

第一,你当专注的是输入的备位置点是一个环。你需要用各个一个Location转换成CLLocation,这里而可利用
func distanceFromLocation(_ location: CLLocation!) ->
CLLocationDistance 这个方法

据悉物理学常识,速度 = 路程 /
时,所以你就算足以得到用户以飞步着各个一样随时速度的更动情况

以此艺术是私家方法,只能以近似中调用。然后,添加这个方法

class func colorSegments(forLocations locations: [Location]) -> [MulticolorPolylineSegment] {
    var colorSegments = [MulticolorPolylineSegment]()

    // RGB for Red (slowest)
    let red   = (r: 1.0, g: 20.0 / 255.0, b: 44.0 / 255.0)

    // RGB for Yellow (middle)
    let yellow = (r: 1.0, g: 215.0 / 255.0, b: 0.0)

    // RGB for Green (fastest)
    let green  = (r: 0.0, g: 146.0 / 255.0, b: 78.0 / 255.0)

    let (speeds, minSpeed, maxSpeed) = allSpeeds(forLocations: locations)

    // now knowing the slowest+fastest, we can get mean too
    let meanSpeed = (minSpeed + maxSpeed)/2

    return colorSegments
  }

此间,你定义了三种颜色分别代表慢速、中速、快速。每一样种植颜色,分别发出其的RGB值得范围,最缓慢得有是清一色红,最抢是都绿色,中速是纯黄色,其它时候颜色会依据速度大小在红->黄色->绿色时渐变,所以最后显示出的结果自然会大灿烂

图片 10

消专注的是公怎样从allspeeds这个元组中以到最好要命价值、最小值和平均值

末,在方之计的末端到return colorSegments 之前在这段代码

for i in 1..<locations.count {
      let l1 = locations[i-1]
      let l2 = locations[i]

      var coords = [CLLocationCoordinate2D]()

      coords.append(CLLocationCoordinate2D(latitude: l1.latitude.doubleValue, longitude: l1.longitude.doubleValue))
      coords.append(CLLocationCoordinate2D(latitude: l2.latitude.doubleValue, longitude: l2.longitude.doubleValue))

      let speed = speeds[i-1]
      var color = UIColor.blackColor()

      if speed < minSpeed { // Between Red & Yellow
        let ratio = (speed - minSpeed) / (meanSpeed - minSpeed)
        let r = CGFloat(red.r + ratio * (yellow.r - red.r))
        let g = CGFloat(red.g + ratio * (yellow.g - red.g))
        let b = CGFloat(red.r + ratio * (yellow.r - red.r))
        color = UIColor(red: r, green: g, blue: b, alpha: 1)
      }
      else { // Between Yellow & Green
        let ratio = (speed - meanSpeed) / (maxSpeed - meanSpeed)
        let r = CGFloat(yellow.r + ratio * (green.r - yellow.r))
        let g = CGFloat(yellow.g + ratio * (green.g - yellow.g))
        let b = CGFloat(yellow.b + ratio * (green.b - yellow.b))
        color = UIColor(red: r, green: g, blue: b, alpha: 1)
      }

      let segment = MulticolorPolylineSegment(coordinates: &coords, count: coords.count)
      segment.color = color
      colorSegments.append(segment)
    }

以此处,你可将到事先计算的速度值、速度的限定,也不怕足以由速度变化之快程度来规定颜色变化之浓度程度

属下,你可因两独如对应的坐标和颜色创建一个初的MulticolorPolylineSegment。最后,你收集及具有的水彩有后,就可准备开渲染了

Applying the Multicolored Segments

相思只要为detail View Controller使用新的 multicolor polyline
很简短,打开DetailViewController.swift,找到 loadMap() 方法,将

mapView.addOverlay(polyline())

替换成

let colorSegments = MulticolorPolylineSegment.colorSegments(forLocations: run.locations.array as! [Location])

mapView.addOverlays(colorSegments)

此间创办了一个segments的累累组,并且把拥有overlays加至了map上

最后,你如未雨绸缪为polyline上面每个segment渲染成特定的颜料,所以,用脚的代码重写你的mapView方法

func mapView(mapView:MKMapView!, rendererForOverlay Overlay:MKOverlay!)->NKOverlayRenderer!{
    if !overlay.isKindOfClass(MulticolorPolylineSegment) {
      return nil
    }

    let polyline = overlay as! MulticolorPolylineSegment
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.strokeColor = polyline.color
    renderer.lineWidth = 3
    return renderer
}

圈起和前面的万分像,但是今,每一个segment都于渲染成了特定的颜料

还编译运行,你尽管能够看到这么一个色彩丰富的地图显示了

图片 11

Leaving a Trail Of Breadcrumbs

末段生成的地图看起非常灿烂,但是于跑过程被它是哪些的呢

打开Main.storyboard 找到New Run Scene,拖一个MapKit View进来到“Ready to
launch” label和start button之间

图片 12

然后,为它们长约束

  • 顶部距离label 20 point
  • 地步距离button 20 point
  • 左右偏离superView都为0

下一场打开 NewRunViewController.swift 添加

import MapKit

继而,添加成员属性

@IBOutlet weak var mapView:MKMapView!

于 viewWillAppear 方法中上加

mapView.hidden = true

要地图开始时处于hidden状态,在startPressed 方法末尾添加

mapView.hidden = false

点击start的当儿地图出现

在文书末尾添加 class extension 实现代理方

// MARK: - MKMapViewDelegate
extension NewRunViewController: MKMapViewDelegate {
  func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
    if !overlay.isKindOfClass(MKPolyline) {
      return nil
    }

    let polyline = overlay as! MKPolyline
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.strokeColor = UIColor.blueColor()
    renderer.lineWidth = 3
    return renderer
  }
}

此跟 run details screen 里地非常像,但是这里的stroke color仍然是蓝色之

连片下,你要写代码去创新地图的亮区域,并且以各级发生一个使得地Location的时光写轨迹,将您的locationManager(_:didUpdateLocations:)方法的兑现创新成为

func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
    for location in locations as! [CLLocation] {
      let howRecent = location.timestamp.timeIntervalSinceNow

      if abs(howRecent) < 10 && location.horizontalAccuracy < 20 {
        //update distance
        if self.locations.count > 0 {
          distance += location.distanceFromLocation(self.locations.last)

          var coords = [CLLocationCoordinate2D]()
          coords.append(self.locations.last!.coordinate)
          coords.append(location.coordinate)

          let region = MKCoordinateRegionMakeWithDistance(location.coordinate, 500, 500)
          mapView.setRegion(region, animated: true)

          mapView.addOverlay(MKPolyline(coordinates: &coords, count: coords.count))
        }

        //save location
        self.locations.append(location)
      }
    }
  }

本,你手上的职位一直在地形图的最好核心,同时,蓝色的倒轨迹就你的移位在频频延长

开拓Main.storyboard找到NewRunScene,连接mapView 到map
View,并且安装代理为眼前控制器

编译运行,将会晤看地图实时更新

图片 13

Where To Go From Here

此地有这例子的整体代码
http://cdn4.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Part1-Final.zip

乃可看看哪些用Core
Data存储数据,怎么样当地图及亮详细的奔走信息,这是以此App最基本的有些

若果你的技巧于好之话语,你可以试怎么样使用海拔高度信息,怎么样改变轨道宽度,怎么样使用同样有点截的平分速度要颜色变化比较前越来越流畅

in any
case,这篇教程还会时有发生第二局部,为你介绍为每个用户定制的徽章奖励体制

注:

1.本文翻译自
http://www.raywenderlich.com/97944/make-app-like-runkeeper-swift-part-1

2.原博part 2 已更新,着急的同学可优先查看
http://www.raywenderlich.com/97945/make-app-like-runkeeper-swift-part-2

  • 莫期送及iOS最新教程,但力量有限,翻译不准的地方还望指正

简书:@西木

微博:@角落里之monster

相关文章