翻譯自:https://www.raywenderlich.com/155774/make-app-like-runkeeper-part-2-2
更新提醒:本教程已由 Richard Critz 更新到 iOS 11 Beta 1, Xcode 9 和 Swift 4。原作者為Matt Luedke。

這是教你如何開發(fā)一款類Runkeeper跑步應(yīng)用教程的第二部分也是最后一部分, 完成 顏色編碼地圖和徽章系統(tǒng)!
在本教程的第一部分, 你已經(jīng)創(chuàng)建了帶有如下功能的app:
使用? Core Location 追蹤路線.
地圖上顯示路徑及記錄跑步時(shí)的平均速度.
當(dāng)跑步結(jié)束后顯示一個(gè)路線地圖. 不同顏色的線段表示不同的速度.
當(dāng)前完成的應(yīng)用,足以記錄和顯示數(shù)據(jù), 但要激勵(lì)用戶跑步需要更多的鼓勵(lì).
在本部分, 你將會(huì)通過實(shí)現(xiàn)徽章系統(tǒng)來完成MoonRunner應(yīng)用,這個(gè)徽章系統(tǒng)體現(xiàn)了健身是一種樂趣以及進(jìn)步的成就.
功能如下:
地圖上標(biāo)出距離增長(zhǎng)的檢查點(diǎn)的列表用于激勵(lì)用戶.
當(dāng)用戶跑步時(shí),app顯示即將獎(jiǎng)勵(lì)的徽章縮略圖及獲取徽章所需剩余距離.
用戶首次到達(dá)檢查點(diǎn), app 獎(jiǎng)勵(lì) 徽章并記錄跑步的平均速度.
從那里開始, 以更快速度到達(dá)檢查點(diǎn)將再次被授予銀版和金版徽章.
跑后地圖會(huì)沿著路徑在每一個(gè)檢查點(diǎn)顯示一個(gè)圓點(diǎn),點(diǎn)擊圓點(diǎn)可以展示徽章的名字和圖片.
假如你已經(jīng)完成了教程的第一部分, 你可以在第一部分的項(xiàng)目基礎(chǔ)上繼續(xù)項(xiàng)目開發(fā). 如果你直接從部分開始, 下載本部分項(xiàng)目模板.
不管你使用什么文件, 你將會(huì)注意到你的項(xiàng)目包括asset下的圖片文件和一個(gè)badges.txt文件. 現(xiàn)在打開badges.txt.? 文件中包括徽章對(duì)象的JSON數(shù)組. 每個(gè)對(duì)象包括:
名稱.
徽章的一些有用信息.
獲得徽章的距離, 以米為單位.
在asset目錄中對(duì)應(yīng)的圖片文件名.
徽章記錄從 0 米開始 — 嘿, 你必須從某個(gè)地方開始 — 直到整個(gè)馬拉松結(jié)束.
第一個(gè)任務(wù)就是將JSON文本解析為徽章對(duì)象數(shù)組. 項(xiàng)目中添加一個(gè)文件并命名為Badge.Swift, 添加如下實(shí)現(xiàn)代碼:
[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結(jié)構(gòu)并提供了一個(gè)從JSON對(duì)象提取信息的 可返回失敗構(gòu)造器.
在結(jié)構(gòu)體中添加如下屬性用于讀取和解析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反序列化工具從文件中解析數(shù)據(jù)并用flatMap過濾掉初始化失敗的對(duì)象.allBadges聲明為static是為了保證損耗性能的解析操作只執(zhí)行一次.
你需要進(jìn)行徽章的比較, 在文件末尾添加如下擴(kuò)展:
[plain]view plaincopy
extension?Badge:?Equatable?{
static?func?==(lhs:?Badge,?rhs:?Badge)?->?Bool?{
return?lhs.name?==?rhs.name
}
}
現(xiàn)在已經(jīng)創(chuàng)建了Badge結(jié)構(gòu)體, 你需要一個(gè)結(jié)構(gòu)體來存儲(chǔ)已經(jīng)獲得的徽章. 此結(jié)構(gòu)體將Badge和各種Run對(duì)象(如果有的話)關(guān)聯(lián)起來, 用戶可以讀取已經(jīng)獎(jiǎng)勵(lì)的徽章的版本.
在項(xiàng)目中添加一個(gè)文件并命名為: BadgeStatus.swift, 實(shí)現(xiàn)代碼如下:
[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結(jié)構(gòu)體 和 用戶提高多少時(shí)間獲取銀版或者金版徽章的乘數(shù). 接著在結(jié)構(gòu)體中添加如下方法:
[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)
}
}
本方法將用戶的每次跑步任務(wù)與取得徽章達(dá)到的距離進(jìn)行比較,從而使每個(gè)徽章關(guān)聯(lián)并返回每個(gè)獲得的徽章的BadgeStatus數(shù)組.
用戶首次獲得徽章時(shí),作為參考速度將成為用于確定后續(xù)運(yùn)行是否有足夠的提升以獲得銀版或金版徽章.
最后, 該方法跟蹤用戶到每個(gè)徽章距離的最快速度.
到目前為止,你已經(jīng)實(shí)現(xiàn)了獲取徽章獎(jiǎng)勵(lì)的邏輯, 現(xiàn)在向用戶展示他們. 項(xiàng)目模板中已經(jīng)定義了必須的UI界面. 你需要在一個(gè)UITableViewController顯示徽章列表. 要想顯示內(nèi)容, 首先,你需要自定義顯示徽章的table view cell.
添加一個(gè)命名為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 將會(huì)顯示徽章信息. 定義的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
}
}
這個(gè)簡(jiǎn)單的方法通過設(shè)置的BadgeStatus來配置table view cell.
如果你拷貝和粘貼代碼, 你會(huì)發(fā)現(xiàn)Xcode 會(huì)把#colorLiterals 轉(zhuǎn)變?yōu)轭伾甘緣K. 如果你是打字輸入, 鍵入單詞Color literal, 選中完成并雙擊顏色指示塊.

將會(huì)顯示一個(gè)簡(jiǎn)單的拾色器. 點(diǎn)擊Other…按鈕.

這將會(huì)調(diào)用系統(tǒng)拾色器. 在示例項(xiàng)目中匹配顏色, 使用Hex Color #域 并 輸入FF142C表示紅色 ,00924E表示綠色.

打開Main.storyboard同時(shí) 在Badges Table View Controller Scene中 將outlets 關(guān)聯(lián)到BadgeCell:
badgeImageView
silverImageView
goldImageView
nameLabel
earnedLabel
目前為止,已經(jīng)定義好table cell, 我們現(xiàn)在創(chuàng)建table view controller.? 在項(xiàng)目中添加一個(gè)命名為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?[]
}
}
}
當(dāng)視圖加載時(shí), 從Core Data 中讀取已經(jīng)完成的跑步任務(wù)列表, 按時(shí)間排序, 接著使用它創(chuàng)建獲取的徽章列表.
下一步, 在擴(kuò)展中添加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
}
}
這些標(biāo)準(zhǔn)的UITableViewDataSource方法是所有UITableViewController必須的, 他們分別返回行數(shù) 和生成的cell.
編譯并運(yùn)行來獲取你的新徽章! 你將會(huì)看到如下樣子的界面:

MoonRunner最后一個(gè)頁(yè)面是展示徽章詳細(xì)信息的. 項(xiàng)目中添加一個(gè)命名為BadgeDetailsViewController.swift的文件. 使用如下代碼替換文件內(nèi)容:
[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頁(yè)面展示控制,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讀取數(shù)據(jù)并設(shè)置詳細(xì)頁(yè)面中的標(biāo)簽文本. 現(xiàn)在, 設(shè)置 金版和銀版徽章.
在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!"
}
金版和銀版徽章圖像如果需要隱藏時(shí)可通過設(shè)置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)
}
當(dāng)按下info按鈕是此方法就會(huì)被調(diào)用同時(shí)顯示一個(gè) 帶有徽章信息的彈出窗口.
打開Main.storyboard. 將BadgeDetailsViewController和 outlets 進(jìn)行關(guān)聯(lián) :
badgeImageView
nameLabel
distanceLabel
earnedLabel
bestLabel
silverLabel
goldLabel
silverImageView
goldImageView
info按鈕 關(guān)聯(lián)響應(yīng)事件:infoButtonTapped(). 最后, 在Badges Table View Controller Scene中選擇Table View.

Attributes Inspector中 選中User Interaction Enabled:

打開BadgesTableViewController.swift并添加如下擴(kuò)展:
[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
}
}
}
當(dāng)用戶在列表中按下一個(gè)徽章,它負(fù)責(zé)將BadgeStatus傳遞給BadgeDetailsViewController.
iOS 11 注意:當(dāng)前 beta 版本的 iOS 11在cell配置后或者顯示之前會(huì)把UserInteractionEnabled重置為true. 因此,你必須實(shí)現(xiàn)shouldPerformSegue(withIdentifier:sender:)來防止訪問未獲得的徽章的詳細(xì)信息. 如果iOS11的后續(xù)版本修復(fù)此bug, 這個(gè)方法可以刪除掉.
編譯并運(yùn)行. 獲取徽章詳情!

現(xiàn)在,你已經(jīng)實(shí)現(xiàn)了一個(gè)很酷的徽章系統(tǒng), 你需要將其融合到現(xiàn)有app的UI更新中. 在完成這個(gè)之前,你需要幾個(gè)實(shí)用方法來確定給定距離下的最近獲得的徽章和下一個(gè)將要獲得的徽章.
打開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!
}
這些方法都會(huì)根據(jù)已經(jīng)獲得或尚未獲得徽章來過濾徽章列表.
現(xiàn)在, 打開Main.storyboard. 在New Run View Controller Scene找到Button Stack View. 將一個(gè)UIImageView和一個(gè)UILabel拖拽到視圖大綱中. 確保他們?cè)?i>Button Stack View的頂部:

選中兩個(gè)控件并且選擇Editor\Embed In\Stack View.按照如下取值修改屬性值:
Axis:Horizontal
Distribution:Fill Equally
Spacing:10
Hidden:checked

設(shè)置圖像的Content Mode為Aspect 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 去關(guān)聯(lián)outlet , Image View 和 Label并命名為:
[plain]view plaincopy
@IBOutlet?weak?var?badgeStackView:?UIStackView!
@IBOutlet?weak?var?badgeImageView:?UIImageView!
@IBOutlet?weak?var?badgeInfoLabel:?UILabel!
Xcode 9 注意:如果你發(fā)現(xiàn)一對(duì)由新控件的垂直位置因歧義引起的警告, 請(qǐng)不要擔(dān)心. 你的Xcode版本沒有正確計(jì)算隱藏子視圖的布局. 要想消除警告,? 在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的發(fā)布版中解決.
打開NewRunViewController.swift并導(dǎo)入 AVFoundation:
[plain]view plaincopy
import?AVFoundation
現(xiàn)在, 添加如下屬性:
[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)
}()
當(dāng)每次獲得一個(gè)新徽章,successSound是 一個(gè)音頻播放器 用于 播放? "成功聲音"
接著, 找到updateDisplay()添加如下代碼:
[plain]view plaincopy
let?distanceRemaining?=?upcomingBadge.distance?-?distance.value
let?formattedDistanceRemaining?=?FormatDisplay.distance(distanceRemaining)
badgeInfoLabel.text?=?"\(formattedDistanceRemaining)?until?\(upcomingBadge.name)"
這個(gè)用于更新即將獲得的徽章.
在startRun(), 調(diào)用updateDisplay()之前, 添加:
[plain]view plaincopy
badgeStackView.isHidden?=?false
upcomingBadge?=?Badge.next(for:?0)
badgeImageView.image?=?UIImage(named:?upcomingBadge.imageName)
這個(gè)會(huì)展示將要獲得的徽章.
在stopRun()中 添加:
[plain]view plaincopy
badgeStackView.isHidden?=?true
如同其他視圖中一樣, 所有的徽章在跑步期間應(yīng)該被隱藏.
添加如下新方法:
[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)
}
}
該方法用于檢查何時(shí)會(huì)獲得, 更新UI并展示下一個(gè)徽章, 同時(shí)播放一個(gè)勝利聲音慶祝獲得一個(gè)徽章.
在eachSecond()中 在 調(diào)用toupdateDisplay():之前 添加對(duì)checkNextBadge()的調(diào)用
[plain]view plaincopy
checkNextBadge()
編譯并運(yùn)行,隨著模擬器模擬跑步觀察標(biāo)簽的更新. 當(dāng)獲得一個(gè)徽章時(shí)聽播放的聲音!

注意:在控制臺(tái)中, 一旦播放 成功的聲音, 你將會(huì)看到如下的錯(cuò)誤信息:
[aqme] 254: AQDefaultDevice (188): skipping input stream 0 0 0x0
模擬器上這是正常的. 這個(gè)信息來自 AVFoundation,對(duì)于你來說這并不算是個(gè)錯(cuò)誤.
同樣, 如果你不想呆呆的等著測(cè)試徽章, 你可以在模擬器的Debug\Location菜單切換到不同的位置模式. 別擔(dān)心, 我們不會(huì)告訴任何人. :]
當(dāng)存在一個(gè)“空間模式”時(shí),一切都會(huì)變得更好
在一個(gè)跑步任務(wù)結(jié)束后, 提供一個(gè)讓用戶可以看到最新獲得的徽章的功能會(huì)更好.
打開Main.storyboard找到Run Details View Controller Scene. 在 Map View頂層拖拽一個(gè)UIImageView. 從Image View Control+拖拽 到 Map View. 在彈出窗口中,選擇Top,Bottom,Leading和Trailing. 點(diǎn)擊Add Constraints將Image View 邊關(guān)聯(lián)到 Map View邊.

Xcode 將會(huì)添加約束, 每個(gè)的值為0. 然而目前, Image View 并沒有完全覆蓋 Map View,因此你會(huì)看到黃色警告線. 點(diǎn)擊Update Frames按鈕 (底部紅色框標(biāo)注) 來調(diào)整 Image View的大小.

在Image View上拖拽一個(gè)UIButton. 刪除按鈕的標(biāo)題并設(shè)置Image值為info.

從 button Control+拖拽 到 Image View. 在彈出窗口中選擇Bottom和Trailing. 點(diǎn)擊Add Constraints將按鈕關(guān)聯(lián)到 image view 的右下角.

在Size Inspector中, 編輯每個(gè) constraint 設(shè)置值為-8.

再次點(diǎn)擊Update Frames按鈕 修復(fù)按鈕的大小和位置.

選中 Image View 并設(shè)置Content Mode為Aspect Fit,Alpha為0.

選中 按鈕設(shè)置Alpha為0.
注意:你應(yīng)該使用Alpha 屬性來隱藏這些視圖而不是使用 Hidden 屬性 因?yàn)檫@樣將會(huì)啟動(dòng)動(dòng)畫效果讓用戶獲得更加流暢的用戶體驗(yàn).
在視圖的后下角添加一個(gè)UISwitch和一個(gè)UILabel.

選中Switch并按下Add New Contraints按鈕 (鈦戰(zhàn)機(jī)按鈕). 添加約束Right,Bottom和Left設(shè)置值為8. 確保Left約束相對(duì)于Label. 選擇Add 3 Constraints.

Set the SwitchValuetoOff.

從 Swith Control+拖拽到Label. 在彈出窗口中選擇Center Vertically.

選中 Label, 設(shè)置標(biāo)題為SPACE MODE及顏色為White Color.

在視圖大綱中, 從Switch Control+拖拽 到 Stack View. 在彈出窗口中選擇Vertical Spacing.

在? Switch 的 Size Inspector 中 ,編輯約束Top Space to: Stack View. 設(shè)置關(guān)系為≥值為8.

喲! 在所有的布局工作完成之后你獲得一個(gè)徽章! :]
在 Assistant Editor中 打開RunDetailsViewController.swift為 Image View 和 Info Button 做 outlets關(guān)聯(lián):
[plain]view plaincopy
@IBOutlet?weak?var?badgeImageView:?UIImageView!
@IBOutlet?weak?var?badgeInfoButton:?UIButton!
為Switch 添加 事件響應(yīng):
[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
}
}
當(dāng) switch 值改變, 你可以通過改變alpha值改變 Image View, Info Button 和? Map View的可見性.
現(xiàn)在,為Info Button 添加響應(yīng)事件:
[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中的按鈕響應(yīng)事件類似.
最后一步是在configureView()方法末尾添加如下代碼:
[plain]view plaincopy
let?badge?=?Badge.best(for:?run.distance)
badgeImageView.image?=?UIImage(named:?badge.imageName)
當(dāng)用戶跑步的時(shí)候你可以找到用戶獲得的最新的徽章并展示出來.
編譯并運(yùn)行. 在模擬器上啟動(dòng)跑步, 保存信息并嘗試你的“太空模式”!

跑后的地圖已經(jīng)幫助你記錄你的路線和展示速度較慢的區(qū)域. 現(xiàn)在 你將要添加一個(gè)功能:精確的展示每個(gè)徽章是從哪里獲得的.
MapKit 使用 annotations 來展示數(shù)據(jù)點(diǎn). 要想創(chuàng)建annotations, 你需要:
一個(gè)遵守MKAnnotation協(xié)議的類,用于提供描述annotation位置的坐標(biāo).
一個(gè)MKAnnotationView的子類用于顯示關(guān)聯(lián)annotation的信息.
你需要實(shí)現(xiàn)這些:
創(chuàng)建類BadgeAnnotation其遵守MKAnnotation協(xié)議.
創(chuàng)建一個(gè)存儲(chǔ)BadgeAnnotation對(duì)象的數(shù)組并將其添加到地圖上.
實(shí)現(xiàn)mapView(_:viewFor:)用戶創(chuàng)建MKAnnotationViews.
添加一個(gè)文件命名為BadgeAnnotation.swift. 替換代碼如下:
[plain]view plaincopy
import?MapKit
class?BadgeAnnotation:?MKPointAnnotation?{
let?imageName:?String
init(imageName:?String)?{
self.imageName?=?imageName
super.init()
}
}
MKPointAnnotation遵守MKAnnotation協(xié)議,你需要一種方式為渲染系統(tǒng)傳入圖片名字.
打開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
}
這段代碼創(chuàng)建了一個(gè)存儲(chǔ)BadgeAnnotation對(duì)象的數(shù)組, 每一個(gè)徽章時(shí)在跑步時(shí)獲得.
在loadMap()末尾添加如下代碼:
[plain]view plaincopy
mapView.addAnnotations(annotations())
這行代碼將annotations添加到地圖上.
最后, 添加如下擴(kuò)展:
[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
}
這里, 你為每個(gè)annotation創(chuàng)建一個(gè)MKAnnotationView并設(shè)置顯示徽章的圖像.
編譯并運(yùn)行. 在模擬器上啟動(dòng)一個(gè)跑步任務(wù)并在最后保存跑步信息. 地圖上將會(huì)展示每個(gè)獲得的徽章的annotation. 點(diǎn)擊一個(gè)你將會(huì)看到 名稱, 圖片 和 距離.

在這個(gè)包含兩部分的教程中 ,你開發(fā)了一個(gè)應(yīng)用程序:
使用Core Location 度量和跟蹤你的跑步任務(wù).
顯示實(shí)時(shí)數(shù)據(jù), 如跑步的平均速度,還有一張動(dòng)態(tài)地圖.
在地圖上展示彩色編碼的線段并自定義每個(gè)檢查點(diǎn)的annotation.
基于距離和速度的個(gè)人進(jìn)程的獎(jiǎng)勵(lì)徽章.
還有很多功能需要你去實(shí)現(xiàn):
為用戶添加歷史跑步列表.NSFetchedResultsController和 現(xiàn)有的RunDetailsViewController使這成為小菜一碟!
計(jì)算每?jī)蓚€(gè)檢查點(diǎn)之間的平均速度并在MKAnnotationView進(jìn)行展示.
感謝您的閱讀. 一如既往, 期待您的意見和問題! :]