翻譯自: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 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 去關聯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,Leading和Trailing. 點擊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. 在彈出窗口中選擇Bottom和Trailing. 點擊Add Constraints將按鈕關聯到 image view 的右下角.
在Size Inspector中, 編輯每個 constraint 設置值為-8.
再次點擊Update Frames按鈕 修復按鈕的大小和位置.
選中 Image View 并設置Content Mode為Aspect Fit,Alpha為0.
選中 按鈕設置Alpha為0.
注意:你應該使用Alpha 屬性來隱藏這些視圖而不是使用 Hidden 屬性 因為這樣將會啟動動畫效果讓用戶獲得更加流暢的用戶體驗.
在視圖的后下角添加一個UISwitch和一個UILabel.
選中Switch并按下Add New Contraints按鈕 (鈦戰機按鈕). 添加約束Right,Bottom和Left設置值為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進行展示.
感謝您的閱讀. 一如既往, 期待您的意見和問題! :]