怎么样创建一个像RunKeeper一样的app(二)swift版
怎么样创建一个像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
如果你还没有看过第一部分的教程,可以查看我之前的博文
在项目文件的配置中已经包含了一个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 来初始化
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 的奖励
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和奖励标识如图所示
如果你已经用过它来跑步的话,肯定已经获得了 earth 级别的奖励,显然,奖励才刚开始
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按钮设置点击事件
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
新的界面长长这样
新的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在不断更新
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有进一步的提升,你还需要做
- 显示用户的跑步历史记录
- 将奖励和速度值标注在轨迹上
- 将奖励和注释标注在地图上