iOS-如何开发一款类 Runkeeper 的跑步应用 (下)

翻译自:https://www.raywenderlich.com/155774/make-app-like-runkeeper-part-2-2

更新提醒:本教程已由 Richard Critz 更新到 iOS 11 Beta 1, Xcode 9 和 Swift 4。原作者为Matt Luedke。

这是教你如何开发一款类Runkeeper跑步应用教程的第二部分也是最后一部分, 完成 颜色编码地图和徽章系统!

本教程的第一部分, 你已经创建了带有如下功能的app:

使用  Core Location 追踪路线.

地图上显示路径及记录跑步时的平均速度.

当跑步结束后显示一个路线地图. 不同颜色的线段表示不同的速度.

当前完成的应用,足以记录和显示数据, 但要激励用户跑步需要更多的鼓励.

在本部分, 你将会通过实现徽章系统来完成MoonRunner应用,这个徽章系统体现了健身是一种乐趣以及进步的成就.

功能如下:

地图上标出距离增长的检查点的列表用于激励用户.

当用户跑步时,app显示即将奖励的徽章缩略图及获取徽章所需剩余距离.

用户首次到达检查点, app 奖励 徽章并记录跑步的平均速度.

从那里开始, 以更快速度到达检查点将再次被授予银版和金版徽章.

跑后地图会沿着路径在每一个检查点显示一个圆点,点击圆点可以展示徽章的名字和图片.

开始

假如你已经完成了教程的第一部分, 你可以在第一部分的项目基础上继续项目开发. 如果你直接从部分开始, 下载本部分项目模板.

不管你使用什么文件, 你将会注意到你的项目包括asset下的图片文件和一个badges.txt文件. 现在打开badges.txt.  文件中包括徽章对象的JSON数组. 每个对象包括:

名称.

徽章的一些有用信息.

获得徽章的距离, 以米为单位.

在asset目录中对应的图片文件名.

徽章记录从 0 米开始 — 嘿, 你必须从某个地方开始 — 直到整个马拉松结束.

第一个任务就是将JSON文本解析为徽章对象数组. 项目中添加一个文件并命名为Badge.Swift, 添加如下实现代码:

[plain]view plaincopy

struct Badge {

let name: String

let imageName: String

let information: String

let distance: Double

init?(from dictionary: [String: String]) {

guard

let name = dictionary["name"],

let imageName = dictionary["imageName"],

let information = dictionary["information"],

let distanceString = dictionary["distance"],

let distance = Double(distanceString)

else {

return nil

}

self.name = name

self.imageName = imageName

self.information = information

self.distance = distance

}

}

这段代码定义了Badge结构并提供了一个从JSON对象提取信息的 可返回失败构造器.

在结构体中添加如下属性用于读取和解析JSON:

[plain]view plaincopy

static let allBadges: [Badge] = {

guard let fileURL = Bundle.main.url(forResource: "badges", withExtension: "txt") else {

fatalError("No badges.txt file found")

}

do {

let jsonData = try Data(contentsOf: fileURL, options: .mappedIfSafe)

let jsonResult = try JSONSerialization.jsonObject(with: jsonData) as! [[String: String]]

return jsonResult.flatMap(Badge.init)

} catch {

fatalError("Cannot decode badges.txt")

}

}()

你可以使用基本的JSON反序列化工具从文件中解析数据并用flatMap过滤掉初始化失败的对象.allBadges声明为static是为了保证损耗性能的解析操作只执行一次.

你需要进行徽章的比较, 在文件末尾添加如下扩展:

[plain]view plaincopy

extension Badge: Equatable {

static func ==(lhs: Badge, rhs: Badge) -> Bool {

return lhs.name == rhs.name

}

}

赢得徽章

现在已经创建了Badge结构体, 你需要一个结构体来存储已经获得的徽章. 此结构体将Badge和各种Run对象(如果有的话)关联起来, 用户可以读取已经奖励的徽章的版本.

在项目中添加一个文件并命名为: BadgeStatus.swift, 实现代码如下:

[plain]view plaincopy

struct BadgeStatus {

let badge: Badge

let earned: Run?

let silver: Run?

let gold: Run?

let best: Run?

static let silverMultiplier = 1.05

static let goldMultiplier = 1.1

}

此处定义了BadgeStatus结构体 和 用户提高多少时间获取银版或者金版徽章的乘数. 接着在结构体中添加如下方法:

[plain]view plaincopy

static func badgesEarned(runs: [Run]) -> [BadgeStatus] {

return Badge.allBadges.map { badge in

var earned: Run?

var silver: Run?

var gold: Run?

var best: Run?

for run in runs where run.distance > badge.distance {

if earned == nil {

earned = run

}

let earnedSpeed = earned!.distance / Double(earned!.duration)

let runSpeed = run.distance / Double(run.duration)

if silver == nil && runSpeed > earnedSpeed * silverMultiplier {

silver = run

}

if gold == nil && runSpeed > earnedSpeed * goldMultiplier {

gold = run

}

if let existingBest = best {

let bestSpeed = existingBest.distance / Double(existingBest.duration)

if runSpeed > bestSpeed {

best = run

}

} else {

best = run

}

}

return BadgeStatus(badge: badge, earned: earned, silver: silver, gold: gold, best: best)

}

}

本方法将用户的每次跑步任务与取得徽章达到的距离进行比较,从而使每个徽章关联并返回每个获得的徽章的BadgeStatus数组.

用户首次获得徽章时,作为参考速度将成为用于确定后续运行是否有足够的提升以获得银版或金版徽章.

最后, 该方法跟踪用户到每个徽章距离的最快速度.

徽章显示

到目前为止,你已经实现了获取徽章奖励的逻辑, 现在向用户展示他们. 项目模板中已经定义了必须的UI界面. 你需要在一个UITableViewController显示徽章列表. 要想显示内容, 首先,你需要自定义显示徽章的table view cell.

添加一个命名为BadgeCell.swift 的文件. 替换文件中的代码为:

[plain]view plaincopy

import UIKit

class BadgeCell: UITableViewCell {

@IBOutlet weak var badgeImageView: UIImageView!

@IBOutlet weak var silverImageView: UIImageView!

@IBOutlet weak var goldImageView: UIImageView!

@IBOutlet weak var nameLabel: UILabel!

@IBOutlet weak var earnedLabel: UILabel!

var status: BadgeStatus! {

didSet {

configure()

}

}

}

这些 outlets 将会显示徽章信息. 定义的status变量为cell 的模型.

接着, 在 status 变量 下方添加configure()方法:

[plain]view plaincopy

private let redLabel = #colorLiteral(red: 1, green: 0.07843137255, blue: 0.1725490196, alpha: 1)

private let greenLabel = #colorLiteral(red: 0, green: 0.5725490196, blue: 0.3058823529, alpha: 1)

private let badgeRotation = CGAffineTransform(rotationAngle: .pi / 8)

private func configure() {

silverImageView.isHidden = status.silver == nil

goldImageView.isHidden = status.gold == nil

if let earned = status.earned {

nameLabel.text = status.badge.name

nameLabel.textColor = greenLabel

let dateEarned = FormatDisplay.date(earned.timestamp)

earnedLabel.text = "Earned: \(dateEarned)"

earnedLabel.textColor = greenLabel

badgeImageView.image = UIImage(named: status.badge.imageName)

silverImageView.transform = badgeRotation

goldImageView.transform = badgeRotation

isUserInteractionEnabled = true

accessoryType = .disclosureIndicator

} else {

nameLabel.text = "?????"

nameLabel.textColor = redLabel

let formattedDistance = FormatDisplay.distance(status.badge.distance)

earnedLabel.text = "Run \(formattedDistance) to earn"

earnedLabel.textColor = redLabel

badgeImageView.image = nil

isUserInteractionEnabled = false

accessoryType = .none

selectionStyle = .none

}

}

这个简单的方法通过设置的BadgeStatus来配置table view cell.

如果你拷贝和粘贴代码, 你会发现Xcode 会把#colorLiterals 转变为颜色指示块. 如果你是打字输入, 键入单词Color literal, 选中完成并双击颜色指示块.

将会显示一个简单的拾色器. 点击Other…按钮.

这将会调用系统拾色器. 在示例项目中匹配颜色, 使用Hex Color #域 并 输入FF142C表示红色 ,00924E表示绿色.

打开Main.storyboard同时 在Badges Table View Controller Scene中 将outlets 关联到BadgeCell:

badgeImageView

silverImageView

goldImageView

nameLabel

earnedLabel

目前为止,已经定义好table cell, 我们现在创建table view controller.  在项目中添加一个命名为BadgesTableViewController.swift的文件. import部分替换为importUIKit和CoreData:

[plain]view plaincopy

import UIKit

import CoreData

接着, 添加类定义:

[plain]view plaincopy

class BadgesTableViewController: UITableViewController {

var statusList: [BadgeStatus]!

override func viewDidLoad() {

super.viewDidLoad()

statusList = BadgeStatus.badgesEarned(runs: getRuns())

}

private func getRuns() -> [Run] {

let fetchRequest: NSFetchRequest = Run.fetchRequest()

let sortDescriptor = NSSortDescriptor(key: #keyPath(Run.timestamp), ascending: true)

fetchRequest.sortDescriptors = [sortDescriptor]

do {

return try CoreDataStack.context.fetch(fetchRequest)

} catch {

return []

}

}

}

当视图加载时, 从Core Data 中读取已经完成的跑步任务列表, 按时间排序, 接着使用它创建获取的徽章列表.

下一步, 在扩展中添加UITableViewDataSource方法:

[plain]view plaincopy

extension BadgesTableViewController {

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

return statusList.count

}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell: BadgeCell = tableView.dequeueReusableCell(for: indexPath)

cell.status = statusList[indexPath.row]

return cell

}

}

这些标准的UITableViewDataSource方法是所有UITableViewController必须的, 他们分别返回行数 和生成的cell.

编译并运行来获取你的新徽章! 你将会看到如下样子的界面:

为了获得金牌跑步者应该做些什么?

MoonRunner最后一个页面是展示徽章详细信息的. 项目中添加一个命名为BadgeDetailsViewController.swift的文件. 使用如下代码替换文件内容:

[plain]view plaincopy

import UIKit

class BadgeDetailsViewController: UIViewController {

@IBOutlet weak var badgeImageView: UIImageView!

@IBOutlet weak var nameLabel: UILabel!

@IBOutlet weak var distanceLabel: UILabel!

@IBOutlet weak var earnedLabel: UILabel!

@IBOutlet weak var bestLabel: UILabel!

@IBOutlet weak var silverLabel: UILabel!

@IBOutlet weak var goldLabel: UILabel!

@IBOutlet weak var silverImageView: UIImageView!

@IBOutlet weak var goldImageView: UIImageView!

var status: BadgeStatus!

}

这些outlets 用于UI页面展示控制,BadgeStatus是本视图的模型.

接着, 添加viewDidLoad():

[plain]view plaincopy

override func viewDidLoad() {

super.viewDidLoad()

let badgeRotation = CGAffineTransform(rotationAngle: .pi / 8)

badgeImageView.image = UIImage(named: status.badge.imageName)

nameLabel.text = status.badge.name

distanceLabel.text = FormatDisplay.distance(status.badge.distance)

let earnedDate = FormatDisplay.date(status.earned?.timestamp)

earnedLabel.text = "Reached on \(earnedDate)"

let bestDistance = Measurement(value: status.best!.distance, unit: UnitLength.meters)

let bestPace = FormatDisplay.pace(distance: bestDistance,

seconds: Int(status.best!.duration),

outputUnit: UnitSpeed.minutesPerMile)

let bestDate = FormatDisplay.date(status.earned?.timestamp)

bestLabel.text = "Best: \(bestPace), \(bestDate)"

let earnedDistance = Measurement(value: status.earned!.distance, unit: UnitLength.meters)

let earnedDuration = Int(status.earned!.duration)

}

从BadgeStatus读取数据并设置详细页面中的标签文本. 现在, 设置 金版和银版徽章.

在viewDidLoad()方法末尾添加如下代码:

[plain]view plaincopy

if let silver = status.silver {

silverImageView.transform = badgeRotation

silverImageView.alpha = 1

let silverDate = FormatDisplay.date(silver.timestamp)

silverLabel.text = "Earned on \(silverDate)"

} else {

silverImageView.alpha = 0

let silverDistance = earnedDistance * BadgeStatus.silverMultiplier

let pace = FormatDisplay.pace(distance: silverDistance,

seconds: earnedDuration,

outputUnit: UnitSpeed.minutesPerMile)

silverLabel.text = "Pace < \(pace) for silver!"

}

if let gold = status.gold {

goldImageView.transform = badgeRotation

goldImageView.alpha = 1

let goldDate = FormatDisplay.date(gold.timestamp)

goldLabel.text = "Earned on \(goldDate)"

} else {

goldImageView.alpha = 0

let goldDistance = earnedDistance * BadgeStatus.goldMultiplier

let pace = FormatDisplay.pace(distance: goldDistance,

seconds: earnedDuration,

outputUnit: UnitSpeed.minutesPerMile)

goldLabel.text = "Pace < \(pace) for gold!"

}

金版和银版徽章图像如果需要隐藏时可通过设置alphas 为 0.

最后, 添加如下方法:

[plain]view plaincopy

@IBAction func infoButtonTapped() {

let alert = UIAlertController(title: status.badge.name,

message: status.badge.information,

preferredStyle: .alert)

alert.addAction(UIAlertAction(title: "OK", style: .cancel))

present(alert, animated: true)

}

当按下info按钮是此方法就会被调用同时显示一个 带有徽章信息的弹出窗口.

打开Main.storyboard. 将BadgeDetailsViewController和 outlets 进行关联 :

badgeImageView

nameLabel

distanceLabel

earnedLabel

bestLabel

silverLabel

goldLabel

silverImageView

goldImageView

info按钮 关联响应事件:infoButtonTapped(). 最后, 在Badges Table View Controller Scene中选择Table View.

Attributes Inspector中 选中User Interaction Enabled:

打开BadgesTableViewController.swift并添加如下扩展:

[plain]view plaincopy

extension BadgesTableViewController: SegueHandlerType {

enum SegueIdentifier: String {

case details = "BadgeDetailsViewController"

}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

switch segueIdentifier(for: segue) {

case .details:

let destination = segue.destination as! BadgeDetailsViewController

let indexPath = tableView.indexPathForSelectedRow!

destination.status = statusList[indexPath.row]

}

}

override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {

guard let segue = SegueIdentifier(rawValue: identifier) else { return false }

switch segue {

case .details:

guard let cell = sender as? UITableViewCell else { return false }

return cell.accessoryType == .disclosureIndicator

}

}

}

当用户在列表中按下一个徽章,它负责将BadgeStatus传递给BadgeDetailsViewController.

iOS 11 注意:当前 beta 版本的 iOS 11在cell配置后或者显示之前会把UserInteractionEnabled重置为true. 因此,你必须实现shouldPerformSegue(withIdentifier:sender:)来防止访问未获得的徽章的详细信息. 如果iOS11的后续版本修复此bug, 这个方法可以删除掉.

编译并运行. 获取徽章详情!

胡萝卜式激励

现在,你已经实现了一个很酷的徽章系统, 你需要将其融合到现有app的UI更新中. 在完成这个之前,你需要几个实用方法来确定给定距离下的最近获得的徽章和下一个将要获得的徽章.

打开Badge.swift并添加如下方法:

[plain]view plaincopy

static func best(for distance: Double) -> Badge {

return allBadges.filter { $0.distance < distance }.last ?? allBadges.first!

}

static func next(for distance: Double) -> Badge {

return allBadges.filter { distance < $0.distance }.first ?? allBadges.last!

}

这些方法都会根据已经获得或尚未获得徽章来过滤徽章列表.

现在, 打开Main.storyboard. 在New Run View Controller Scene找到Button Stack View. 将一个UIImageView和一个UILabel拖拽到视图大纲中. 确保他们在Button Stack View的顶部:

选中两个控件并且选择Editor\Embed In\Stack View.按照如下取值修改属性值:

Axis:Horizontal

Distribution:Fill Equally

Spacing:10

Hidden:checked

设置图像的Content ModeAspect Fit.

按照如下取值修改Label的属性值:

Color:White Color

Font:System 14.0

Lines:0

Line Break:Word Wrap

Autoshrink:Minimum Font Size

Tighten Letter Spacing:checked

从新的Stack View中使用Assistant Editor 去关联outlet , Image View 和 Label并命名为:

[plain]view plaincopy

@IBOutlet weak var badgeStackView: UIStackView!

@IBOutlet weak var badgeImageView: UIImageView!

@IBOutlet weak var badgeInfoLabel: UILabel!

Xcode 9 注意:如果你发现一对由新控件的垂直位置因歧义引起的警告, 请不要担心. 你的Xcode版本没有正确计算隐藏子视图的布局. 要想消除警告,  在Main.storyboard 的Badge Stack View中取消选中的 hidden 属性. 接着在NewRunViewController.swift的viewDidLoad()中添加如下代码:

[plain]view plaincopy

badgeStackView.isHidden = true // required to work around behavior change in Xcode 9 beta 1

如果一切顺利的话, 本问题将在Xcode 9的发布版中解决.

打开NewRunViewController.swift并导入 AVFoundation:

[plain]view plaincopy

import AVFoundation

现在, 添加如下属性:

[plain]view plaincopy

private var upcomingBadge: Badge!

private let successSound: AVAudioPlayer = {

guard let successSound = NSDataAsset(name: "success") else {

return AVAudioPlayer()

}

return try! AVAudioPlayer(data: successSound.data)

}()

当每次获得一个新徽章,successSound是 一个音频播放器 用于 播放  "成功声音"

接着, 找到updateDisplay()添加如下代码:

[plain]view plaincopy

let distanceRemaining = upcomingBadge.distance - distance.value

let formattedDistanceRemaining = FormatDisplay.distance(distanceRemaining)

badgeInfoLabel.text = "\(formattedDistanceRemaining) until \(upcomingBadge.name)"

这个用于更新即将获得的徽章.

在startRun(), 调用updateDisplay()之前, 添加:

[plain]view plaincopy

badgeStackView.isHidden = false

upcomingBadge = Badge.next(for: 0)

badgeImageView.image = UIImage(named: upcomingBadge.imageName)

这个会展示将要获得的徽章.

在stopRun()中 添加:

[plain]view plaincopy

badgeStackView.isHidden = true

如同其他视图中一样, 所有的徽章在跑步期间应该被隐藏.

添加如下新方法:

[plain]view plaincopy

private func checkNextBadge() {

let nextBadge = Badge.next(for: distance.value)

if upcomingBadge != nextBadge {

badgeImageView.image = UIImage(named: nextBadge.imageName)

upcomingBadge = nextBadge

successSound.play()

AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)

}

}

该方法用于检查何时会获得, 更新UI并展示下一个徽章, 同时播放一个胜利声音庆祝获得一个徽章.

在eachSecond()中 在 调用toupdateDisplay():之前 添加对checkNextBadge()的调用

[plain]view plaincopy

checkNextBadge()

编译并运行,随着模拟器模拟跑步观察标签的更新. 当获得一个徽章时听播放的声音!

注意:在控制台中, 一旦播放 成功的声音, 你将会看到如下的错误信息:

[aqme] 254: AQDefaultDevice (188): skipping input stream 0 0 0x0

模拟器上这是正常的. 这个信息来自 AVFoundation,对于你来说这并不算是个错误.

同样, 如果你不想呆呆的等着测试徽章, 你可以在模拟器的Debug\Location菜单切换到不同的位置模式. 别担心, 我们不会告诉任何人. :]

当存在一个“空间模式”时,一切都会变得更好

在一个跑步任务结束后, 提供一个让用户可以看到最新获得的徽章的功能会更好.

打开Main.storyboard找到Run Details View Controller Scene. 在 Map View顶层拖拽一个UIImageView. 从Image View Control+拖拽 到 Map View. 在弹出窗口中,选择Top,Bottom,LeadingTrailing. 点击Add Constraints将Image View 边关联到 Map View边.

Xcode 将会添加约束, 每个的值为0. 然而目前, Image View 并没有完全覆盖 Map View,因此你会看到黄色警告线. 点击Update Frames按钮 (底部红色框标注) 来调整 Image View的大小.

在Image View上拖拽一个UIButton. 删除按钮的标题并设置Image值为info.

从 button Control+拖拽 到 Image View. 在弹出窗口中选择BottomTrailing. 点击Add Constraints将按钮关联到 image view 的右下角.

Size Inspector中, 编辑每个 constraint 设置值为-8.

再次点击Update Frames按钮 修复按钮的大小和位置.

选中 Image View 并设置Content ModeAspect FitAlpha0.

选中 按钮设置Alpha0.

注意:你应该使用Alpha 属性来隐藏这些视图而不是使用 Hidden 属性 因为这样将会启动动画效果让用户获得更加流畅的用户体验.

在视图的后下角添加一个UISwitch和一个UILabel.

选中Switch并按下Add New Contraints按钮 (钛战机按钮). 添加约束Right,BottomLeft设置值为8. 确保Left约束相对于Label. 选择Add 3 Constraints.

Set the SwitchValuetoOff.

从 Swith Control+拖拽到Label. 在弹出窗口中选择Center Vertically.

选中 Label, 设置标题为SPACE MODE及颜色为White Color.

在视图大纲中, 从Switch Control+拖拽 到 Stack View. 在弹出窗口中选择Vertical Spacing.

在  Switch 的 Size Inspector 中 ,编辑约束Top Space to: Stack View. 设置关系为值为8.

哟! 在所有的布局工作完成之后你获得一个徽章! :]

在 Assistant Editor中 打开RunDetailsViewController.swift为 Image View 和 Info Button 做 outlets关联:

[plain]view plaincopy

@IBOutlet weak var badgeImageView: UIImageView!

@IBOutlet weak var badgeInfoButton: UIButton!

为Switch 添加 事件响应:

[plain]view plaincopy

@IBAction func displayModeToggled(_ sender: UISwitch) {

UIView.animate(withDuration: 0.2) {

self.badgeImageView.alpha = sender.isOn ? 1 : 0

self.badgeInfoButton.alpha = sender.isOn ? 1 : 0

self.mapView.alpha = sender.isOn ? 0 : 1

}

}

当 switch 值改变, 你可以通过改变alpha值改变 Image View, Info Button 和  Map View的可见性.

现在,为Info Button 添加响应事件:

[plain]view plaincopy

@IBAction func infoButtonTapped() {

let badge = Badge.best(for: run.distance)

let alert = UIAlertController(title: badge.name,

message: badge.information,

preferredStyle: .alert)

alert.addAction(UIAlertAction(title: "OK", style: .cancel))

present(alert, animated: true)

}

这同BadgeDetailsViewController.swift中的按钮响应事件类似.

最后一步是在configureView()方法末尾添加如下代码:

[plain]view plaincopy

let badge = Badge.best(for: run.distance)

badgeImageView.image = UIImage(named: badge.imageName)

当用户跑步的时候你可以找到用户获得的最新的徽章并展示出来.

编译并运行. 在模拟器上启动跑步, 保存信息并尝试你的“太空模式”!

在你的地图上展示徽章

跑后的地图已经帮助你记录你的路线和展示速度较慢的区域. 现在 你将要添加一个功能:精确的展示每个徽章是从哪里获得的.

MapKit 使用 annotations 来展示数据点. 要想创建annotations, 你需要:

一个遵守MKAnnotation协议的类,用于提供描述annotation位置的坐标.

一个MKAnnotationView的子类用于显示关联annotation的信息.

你需要实现这些:

创建类BadgeAnnotation其遵守MKAnnotation协议.

创建一个存储BadgeAnnotation对象的数组并将其添加到地图上.

实现mapView(_:viewFor:)用户创建MKAnnotationViews.

添加一个文件命名为BadgeAnnotation.swift. 替换代码如下:

[plain]view plaincopy

import MapKit

class BadgeAnnotation: MKPointAnnotation {

let imageName: String

init(imageName: String) {

self.imageName = imageName

super.init()

}

}

MKPointAnnotation遵守MKAnnotation协议,你需要一种方式为渲染系统传入图片名字.

打开RunDetailsViewController.swift并添加如下新方法:

[plain]view plaincopy

private func annotations() -> [BadgeAnnotation] {

var annotations: [BadgeAnnotation] = []

let badgesEarned = Badge.allBadges.filter { $0.distance < run.distance }

var badgeIterator = badgesEarned.makeIterator()

var nextBadge = badgeIterator.next()

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

var distance = 0.0

for (first, second) in zip(locations, locations.dropFirst()) {

guard let badge = nextBadge else { break }

let start = CLLocation(latitude: first.latitude, longitude: first.longitude)

let end = CLLocation(latitude: second.latitude, longitude: second.longitude)

distance += end.distance(from: start)

if distance >= badge.distance {

let badgeAnnotation = BadgeAnnotation(imageName: badge.imageName)

badgeAnnotation.coordinate = end.coordinate

badgeAnnotation.title = badge.name

badgeAnnotation.subtitle = FormatDisplay.distance(badge.distance)

annotations.append(badgeAnnotation)

nextBadge = badgeIterator.next()

}

}

return annotations

}

这段代码创建了一个存储BadgeAnnotation对象的数组, 每一个徽章时在跑步时获得.

在loadMap()末尾添加如下代码:

[plain]view plaincopy

mapView.addAnnotations(annotations())

这行代码将annotations添加到地图上.

最后, 添加如下扩展:

[plain]view plaincopy

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

guard let annotation = annotation as? BadgeAnnotation else { return nil }

let reuseID = "checkpoint"

var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID)

if annotationView == nil {

annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseID)

annotationView?.image = #imageLiteral(resourceName: "mapPin")

annotationView?.canShowCallout = true

}

annotationView?.annotation = annotation

let badgeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))

badgeImageView.image = UIImage(named: annotation.imageName)

badgeImageView.contentMode = .scaleAspectFit

annotationView?.leftCalloutAccessoryView = badgeImageView

return annotationView

}

这里, 你为每个annotation创建一个MKAnnotationView并设置显示徽章的图像.

编译并运行. 在模拟器上启动一个跑步任务并在最后保存跑步信息. 地图上将会展示每个获得的徽章的annotation. 点击一个你将会看到 名称, 图片 和 距离.

下一步

你可以下载到本教程的完整项目示例代码.

在这个包含两部分的教程中 ,你开发了一个应用程序:

使用Core Location 度量和跟踪你的跑步任务.

显示实时数据, 如跑步的平均速度,还有一张动态地图.

在地图上展示彩色编码的线段并自定义每个检查点的annotation.

基于距离和速度的个人进程的奖励徽章.

还有很多功能需要你去实现:

为用户添加历史跑步列表.NSFetchedResultsController和 现有的RunDetailsViewController使这成为小菜一碟!

计算每两个检查点之间的平均速度并在MKAnnotationView进行展示.

感谢您的阅读. 一如既往, 期待您的意见和问题! :]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容