翻譯自:https://www.raywenderlich.com/155772/make-app-like-runkeeper-part-1-2
更新提醒:本教程已由 Richard Critz 更新到 iOS 11 Beta 1, Xcode 9 和 Swift 4蹄衷。原作者為Matt Luedke涮俄。
跑步激勵追蹤應(yīng)用Runkeeper目前有4000萬用戶 ! 本教程將教您開發(fā)一款類Runkeeper應(yīng)用,您將會學(xué)到以下內(nèi)容:
使用? Core Location 追蹤路線.
跑步過程中顯示一個地圖并不斷的更新位置.
當(dāng)您跑步時記錄下您的平均速度.
不同距離授予不同的徽章. 無論你的跑步起始點在哪里,每個徽章都由銀色和金色兩種組成,用于表示個人進度.
通過跟蹤到下一級徽章的剩余距離來激勵你.
當(dāng)跑步結(jié)束后顯示一個路線地圖. 不同顏色的線段表示不同的速度.
成果是什么? 開發(fā)一款app —MoonRunner— 徽章系統(tǒng)基于太陽系中的行星和衛(wèi)星!
開始本教程之前, 你應(yīng)該熟悉Storyboards和Core Data. 如果您絕得需要復(fù)習(xí)下知識,請查閱鏈接教程.
本教程同時也使用了iOS10中新增加的Measurement和MeasurementFormatter功能. 更多了解請觀看視頻.
鑒于內(nèi)容眾多,本教程將分為兩部分. 第一部分重點講解 記錄跑步數(shù)據(jù)和地圖路線展示. 第二部分介紹了徽章系統(tǒng).
下載項目模板. 其中包括要完成本教程的所有文件和資源.
花費幾分鐘熟悉下項目.Main.storyboard已經(jīng)包含了 所有UI界面. 將AppDelegate中關(guān)于Core Data的模板代碼移到CoreDataStack.Swift中.Assets.xcassets中包含了將要使用的圖片和聲音文件.
MoonRunner 使用 Core Data 相對簡單, 僅僅使用了兩個實體:Run和Location.
打開MoonRunner.xcdatamodeld同時創(chuàng)建兩個實體:Run和Location. 在Run中添加如下屬性:
Run有三個屬性:distance,duration和timestamp. 其中有一個關(guān)聯(lián),locations, 關(guān)聯(lián)到Location實體.
注意:在下一步之前你不能設(shè)置Inverse關(guān)聯(lián). 這將會引起一個警告. 不要驚慌!
接著,? 給Location添加如下屬性:
Location也有三個屬性:latitude,longitude和timestamp及一個關(guān)聯(lián),run.
選擇關(guān)聯(lián)實體同時驗證locations關(guān)聯(lián)的Inverse屬性 已經(jīng)變?yōu)?i>“run”.
選擇locations關(guān)聯(lián), 設(shè)置Type類型為To Many, 同時在Data Model Inspector’s Relationship的面板 選中Ordered.
最后, 在Data Model Inspector面板中分別驗證Run和Location實體的Codegen屬性 設(shè)置為Class Definition(這是默認(rèn)設(shè)置).
編譯項目讓Xcode 生成Core Data 模型對應(yīng)的swift代碼.
打開RunDetailsViewController.swift,在viewDidLoad()之前添加如下代碼:
[plain]view plaincopy
var?run:?Run!
接著, 在viewDidLoad()之后添加方法:
[plain]view plaincopy
private?func?configureView()?{
}
最后, 在viewDidLoad()中super.viewDidLoad()之后添加configureView().
[plain]view plaincopy
configureView()
這個設(shè)置是app完成導(dǎo)航的最低要求.
打開NewRunViewController.swift并在viewDidLoad()之前添加:
[plain]view plaincopy
private?var?run:?Run?
接著, 添加如下新方法:
[plain]view plaincopy
private?func?startRun()?{
launchPromptStackView.isHidden?=?true
dataStackView.isHidden?=?false
startButton.isHidden?=?true
stopButton.isHidden?=?false
}
private?func?stopRun()?{
launchPromptStackView.isHidden?=?false
dataStackView.isHidden?=?true
startButton.isHidden?=?false
stopButton.isHidden?=?true
}
停止按鈕和UIStackView在storyboard中默認(rèn)為隱藏狀態(tài) . 這些實例用于在 跑步狀態(tài)和非跑步狀態(tài)進行切換.
在startTapped()中添加對startRun()的調(diào)用.
[plain]view plaincopy
startRun()
在文件的底部, 大括號之后, 添加如下擴展:
[plain]view plaincopy
extension?NewRunViewController:?SegueHandlerType?{
enum?SegueIdentifier:?String?{
case?details?=?"RunDetailsViewController"
}
override?func?prepare(for?segue:?UIStoryboardSegue,?sender:?Any?)?{
switch?segueIdentifier(for:?segue)?{
case?.details:
let?destination?=?segue.destination?as!?RunDetailsViewController
destination.run?=?run
}
}
}
大家都知道夫嗓,storyboard 的 segue 是"字符串類型".? segue 標(biāo)識符是一個字符串 并且沒有錯誤檢查.在StoryboardSupport.swift文件中,使用協(xié)議和枚舉及一點點魔法, 你就能避免使用 "字符串類型"帶來的不便.
接著, 在stopTapped()中添加如下代碼:
[plain]view plaincopy
let?alertController?=?UIAlertController(title:?"End?run?",
message:?"Do?you?wish?to?end?your?run?",
preferredStyle:?.actionSheet)
alertController.addAction(UIAlertAction(title:?"Cancel",?style:?.cancel))
alertController.addAction(UIAlertAction(title:?"Save",?style:?.default)?{?_?in
self.stopRun()
self.performSegue(withIdentifier:?.details,?sender:?nil)
})
alertController.addAction(UIAlertAction(title:?"Discard",?style:?.destructive)?{?_?in
self.stopRun()
_?=?self.navigationController?.popToRootViewController(animated:?true)
})
present(alertController,?animated:?true)
當(dāng)用戶按下停止按鈕, 你需要讓他們決定是 保存冲秽,放棄還是繼續(xù). 你可以使用一個UIAlertController彈框來讓用戶做出抉擇.
編譯并運行. 按下 "New Run"按鈕接著再按"Start"按鈕. 驗證 UI界面已經(jīng)變?yōu)榱?“跑步模式”:
按下Stop按鈕 同時 按下Save舍咖,您將進入詳細(xì)頁面.
注意:在控制臺, 你將會看到類似如下的一些錯誤信息:
MoonRunner[5400:226999] [VKDefault] /BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1295.30.5.4.13/src/MDFlyoverAvailability.mm:66: Missing latitudeintrigger specification
這是正常的,對于你而言這并不代表一個錯誤.
ios10 引入了新功能锉桑,使其更容易使用和顯示度量單位. 跑步者度量進度往往采用速度(單位距離消耗的時間)排霉,它是速度(單位時間的距離)的倒數(shù).你必須擴展UnitSpeed來實現(xiàn)這種計算方式.
項目中添加一個文件:UnitExtensions.swift. 在import語句后添加:
[plain]view plaincopy
class?UnitConverterPace:?UnitConverter?{
private?let?coefficient:?Double
init(coefficient:?Double)?{
self.coefficient?=?coefficient
}
override?func?baseUnitValue(fromValue?value:?Double)?->?Double?{
return?reciprocal(value?*?coefficient)
}
override?func?value(fromBaseUnitValue?baseUnitValue:?Double)?->?Double?{
return?reciprocal(baseUnitValue?*?coefficient)
}
private?func?reciprocal(_?value:?Double)?->?Double?{
guard?value?!=?0?else?{?return?0?}
return?1.0?/?value
}
}
在你擴展UnitSpeed的速度轉(zhuǎn)換功能之前, 你必須創(chuàng)建UnitConverter用于數(shù)學(xué)計算.UnitConverter子類需要實現(xiàn)baseUnitValue(fromValue:)和value(fromBaseUnitValue:).
現(xiàn)在, 在文件末尾添加如下代碼
[plain]view plaincopy
extension?UnitSpeed?{
class?var?secondsPerMeter:?UnitSpeed?{
return?UnitSpeed(symbol:?"sec/m",?converter:?UnitConverterPace(coefficient:?1))
}
class?var?minutesPerKilometer:?UnitSpeed?{
return?UnitSpeed(symbol:?"min/km",?converter:?UnitConverterPace(coefficient:?60.0?/?1000.0))
}
class?var?minutesPerMile:?UnitSpeed?{
return?UnitSpeed(symbol:?"min/mi",?converter:?UnitConverterPace(coefficient:?60.0?/?1609.34))
}
}
UnitSpeed是Foundation中 Units下的一個類 .UnitSpeed的默認(rèn)單位為 “米/秒”. 你的擴展中可以讓速度 按照分/千米或分/米來表示.
你需要統(tǒng)一的方式來顯示這些定量信息如距離, 時間, 速度和日期.MeasurementFormatter和DateFormatter使得這些變得簡單.
添加一個Swift 文件并命名為FormatDisplay.swift.import語句后添加以下代碼:
[plain]view plaincopy
struct?FormatDisplay?{
static?func?distance(_?distance:?Double)?->?String?{
let?distanceMeasurement?=?Measurement(value:?distance,?unit:?UnitLength.meters)
return?FormatDisplay.distance(distanceMeasurement)
}
static?func?distance(_?distance:?Measurement)?->?String?{
let?formatter?=?MeasurementFormatter()
return?formatter.string(from:?distance)
}
static?func?time(_?seconds:?Int)?->?String?{
let?formatter?=?DateComponentsFormatter()
formatter.allowedUnits?=?[.hour,?.minute,?.second]
formatter.unitsStyle?=?.positional
formatter.zeroFormattingBehavior?=?.pad
return?formatter.string(from:?TimeInterval(seconds))!
}
static?func?pace(distance:?Measurement,?seconds:?Int,?outputUnit:?UnitSpeed)?->?String?{
let?formatter?=?MeasurementFormatter()
formatter.unitOptions?=?[.providedUnit]?//?1
let?speedMagnitude?=?seconds?!=?0???distance.value?/?Double(seconds)?:?0
let?speed?=?Measurement(value:?speedMagnitude,?unit:?UnitSpeed.metersPerSecond)
return?formatter.string(from:?speed.converted(to:?outputUnit))
}
static?func?date(_?timestamp:?Date?)?->?String?{
guard?let?timestamp?=?timestamp?as?Date??else?{?return?""?}
let?formatter?=?DateFormatter()
formatter.dateStyle?=?.medium
return?formatter.string(from:?timestamp)
}
}
這些簡單的函數(shù)功能不需要過多的解釋. 在pace(distance:seconds:outputUnit:)方法中,? 你必須將MeasurementFormatter的unitOptions設(shè)置為.providedUnits避免它顯示本地化的速度測量單位 (例如 mph 或 kph).
基本上可以開始跑步了. 但是首先, app需要知道它在哪里. 為此, 你將會使用 Core Location. 重要的是,在你的app中只能有一個CLLocationManager實例民轴,它不能被無意中刪除.
為此, 添加一個 Swift 文件攻柠,命名為LocationManager.swift. 將其內(nèi)容替換為:
[plain]view plaincopy
import?CoreLocation
class?LocationManager?{
static?let?shared?=?CLLocationManager()
private?init()?{?}
}
在開始追蹤用戶位置之前球订,你必須做幾個項目級別的修改.
首先, 在項目導(dǎo)航欄頂部點擊項目.
選擇Capabilities欄開啟Background Modes. 選中Location updates.
接著, 打開Info.plist. 點擊緊挨著Information Property List 的 +. 從下拉列表中選擇Privacy – Location When In Use Usage Description同時 設(shè)置其值為 “MoonRunner needs access to your location in order to record and track your run!”
注意:這個Info.plistkey 是非常重要的. 如果沒有它, 你的用戶將不會為你的app來授權(quán)訪問位置服務(wù).
在你的app使用位置信息之前, 設(shè)備必須從用戶那獲得授權(quán). 打開AppDelegate.swift在application(_:didFinishLaunchingWithOptions:)中添加如下代碼,在return true 之前即可:
[plain]view plaincopy
let?locationManager?=?LocationManager.shared
locationManager.requestWhenInUseAuthorization()
打開NewRunViewController.swift并且導(dǎo)入CoreLocation:
[plain]view plaincopy
import?CoreLocation
接著, 在 run屬性后添加如下屬性:
[plain]view plaincopy
private?let?locationManager?=?LocationManager.shared
private?var?seconds?=?0
private?var?timer:?Timer?
private?var?distance?=?Measurement(value:?0,?unit:?UnitLength.meters)
private?var?locationList:?[CLLocation]?=?[]
逐行解釋下:
locationManager是一個對象用戶開啟和關(guān)閉位置服務(wù).
seconds追蹤跑步的時長, 以秒計算.
timer每秒觸發(fā)一次并相應(yīng)的更新UI.
distance存儲累計跑步距離.
locationList是一個數(shù)組瑰钮,用于保存跑步期間所有的CLLocation對象.
viewDidLoad()之后添加以下方法:
[plain]view plaincopy
override?func?viewWillDisappear(_?animated:?Bool)?{
super.viewWillDisappear(animated)
timer?.invalidate()
locationManager.stopUpdatingLocation()
}
當(dāng)用戶離開跑步頁面時冒滩,這確保了timer和帶來大耗電量位置更新的停止.
添加以下兩個方法:
[plain]view plaincopy
func?eachSecond()?{
seconds?+=?1
updateDisplay()
}
private?func?updateDisplay()?{
let?formattedDistance?=?FormatDisplay.distance(distance)
let?formattedTime?=?FormatDisplay.time(seconds)
let?formattedPace?=?FormatDisplay.pace(distance:?distance,
seconds:?seconds,
outputUnit:?UnitSpeed.minutesPerMile)
distanceLabel.text?=?"Distance:??\(formattedDistance)"
timeLabel.text?=?"Time:??\(formattedTime)"
paceLabel.text?=?"Pace:??\(formattedPace)"
}
eachSecond()會被每秒執(zhí)行一次.
updateDisplay()使用FormatDisplay.swift中實現(xiàn)的格式化功能來更新UI.
Core Location 通過CLLocationManagerDelegate記錄位置更新 . 在文件末尾添加擴展:
[plain]view plaincopy
extension?NewRunViewController:?CLLocationManagerDelegate?{
func?locationManager(_?manager:?CLLocationManager,?didUpdateLocations?locations:?[CLLocation])?{
for?newLocation?in?locations?{
let?howRecent?=?newLocation.timestamp.timeIntervalSinceNow
guard?newLocation.horizontalAccuracy?<?20?&&?abs(howRecent)?<?10?else?{?continue?}
if?let?lastLocation?=?locationList.last?{
let?delta?=?newLocation.distance(from:?lastLocation)
distance?=?distance?+?Measurement(value:?delta,?unit:?UnitLength.meters)
}
locationList.append(newLocation)
}
}
}
每次 Core Location 更新用戶位置時這個代理方法就會被調(diào)用, 參數(shù)中有一個存儲CLLocation對象的數(shù)組. 通常這個數(shù)組只包含一個對象, 但是如果有多個, 他們會按照位置更新時間來排序.
CLLocation包含了一些重要信息, 包括經(jīng)度浪谴,維度和時間戳.
在采納讀數(shù)之前, 檢查數(shù)據(jù)的準(zhǔn)確性是值得的. 如果設(shè)備不能確定該讀數(shù)是用戶實際位置20米范圍內(nèi)的, 那么最好將其從數(shù)據(jù)庫中刪除. 確保數(shù)據(jù)是最近的也很重要.
注意:當(dāng)設(shè)備開始縮小用戶區(qū)域時开睡,這種檢查在跑步的開始時尤為重要. 在那個階段,它可能會更新一些頭幾個不準(zhǔn)確的位置數(shù)據(jù).
如果此時的CLLocation數(shù)據(jù)通過了檢查, 那么其與最新記錄點之間的距離與當(dāng)前跑步距離進行累加, 距離以米為單位.
最后, 位置對象添加到不斷增長的位置數(shù)組里.
回到NewRunViewController中添加如下方法(不是擴展中):
[plain]view plaincopy
private?func?startLocationUpdates()?{
locationManager.delegate?=?self
locationManager.activityType?=?.fitness
locationManager.distanceFilter?=?10
locationManager.startUpdatingLocation()
}
你需要實現(xiàn)這個代理苟耻,這樣你能夠接收和處理位置更新.
跑步類應(yīng)用中activityType參數(shù)應(yīng)該這樣設(shè)置. 這樣可以幫助設(shè)備在用戶跑步過程中節(jié)省電量, 比如他們在交叉路口停下來.
最后, 設(shè)置distanceFilter為 10 米.而不像activityType, 這個參數(shù)不會影響電量消耗.
在跑步測試后,您將看到篇恒,位置讀數(shù)可能會偏離直線.distanceFilter值設(shè)置的過高可以減少上下波動,因此可以更加準(zhǔn)確的展示路線. 不幸的是, 值設(shè)置的太高了會是讀數(shù)像素化. 這就是為什么10米是一個折中值.
最后, 啟動 Core Location 進行位置信息更新!
要想開始跑步任務(wù), 在startRun()方法末尾添加如下代碼:
[plain]view plaincopy
seconds?=?0
distance?=?Measurement(value:?0,?unit:?UnitLength.meters)
locationList.removeAll()
updateDisplay()
timer?=?Timer.scheduledTimer(withTimeInterval:?1.0,?repeats:?true)?{?_?in
self.eachSecond()
}
startLocationUpdates()
在跑步狀態(tài)或初始狀態(tài)梁呈,這個將會重置更新的數(shù)據(jù), 啟動Timer用于每秒更新一次并收集位置更新.
某一時刻, 你的用戶感覺累了并停止跑步. UI界面已經(jīng)有讓用戶保存數(shù)據(jù)的功能, 但是你同樣需要自動保存跑步數(shù)據(jù)婚度,否則您的用戶就會因為未保存數(shù)據(jù)所有的努力白費而不高興.
NewRunViewController中添加如下方法:
[plain]view plaincopy
private?func?saveRun()?{
let?newRun?=?Run(context:?CoreDataStack.context)
newRun.distance?=?distance.value
newRun.duration?=?Int16(seconds)
newRun.timestamp?=?Date()
for?location?in?locationList?{
let?locationObject?=?Location(context:?CoreDataStack.context)
locationObject.timestamp?=?location.timestamp
locationObject.latitude?=?location.coordinate.latitude
locationObject.longitude?=?location.coordinate.longitude
newRun.addToLocations(locationObject)
}
CoreDataStack.saveContext()
run?=?newRun
}
如果你使用過Swift3 之前版本的Core Data ,? 你將會發(fā)現(xiàn)iOS 10中對Core Data支持的強大功能和簡潔性. 創(chuàng)建一個 newRun實例并初始化. 接著為每個記錄的CLLocation創(chuàng)建一個Location實例, 只保存相關(guān)的數(shù)據(jù). 最后, 使用自動生成的方法addToLocations(_:)將每個Location添加到newRun中.
當(dāng)用戶結(jié)束跑步, 你需要停止位置追蹤.stopRun()方法末尾添加如下代碼:
[plain]view plaincopy
locationManager.stopUpdatingLocation()
最后, 在stopTapped()方法中定位到UIAlertAction標(biāo)題為"Save"的位置,然后添加方法調(diào)用self.saveRun()官卡,添加后的代碼是這個樣子的:
[plain]view plaincopy
alertController.addAction(UIAlertAction(title:?"Save",?style:?.default)?{?_?in
self.stopRun()
self.saveRun()?//?ADD?THIS?LINE!
self.performSegue(withIdentifier:?.details,?sender:?nil)
})
Send the Simulator On a Run模擬器上模擬跑步
應(yīng)用發(fā)布前,你應(yīng)該在真機上進行測試, 但每次你想測試MoonRunner時醋虏,不必出去跑步.
編譯并運行模擬器. 在按下 “New Run”按鈕之前, 從模擬器菜單中選擇Debug\Location\City Run.
現(xiàn)在, 按下New Run, 接著按下Start寻咒,模擬器已經(jīng)開始模擬跑步.
上述工作完成后, 我們需要展示用戶的目的地和完成情況.
打開RunDetailsViewController.swift同時將configureView()中替換為:
[plain]view plaincopy
private?func?configureView()?{
let?distance?=?Measurement(value:?run.distance,?unit:?UnitLength.meters)
let?seconds?=?Int(run.duration)
let?formattedDistance?=?FormatDisplay.distance(distance)
let?formattedDate?=?FormatDisplay.date(run.timestamp)
let?formattedTime?=?FormatDisplay.time(seconds)
let?formattedPace?=?FormatDisplay.pace(distance:?distance,
seconds:?seconds,
outputUnit:?UnitSpeed.minutesPerMile)
distanceLabel.text?=?"Distance:??\(formattedDistance)"
dateLabel.text?=?formattedDate
timeLabel.text?=?"Time:??\(formattedTime)"
paceLabel.text?=?"Pace:??\(formattedPace)"
}
格式化跑步詳細(xì)信息并顯示.
在地圖上顯示跑步信息有些工作量. 需三步完成:
設(shè)置地圖顯示區(qū)域,僅僅顯示跑步的區(qū)域而不是整個世界地圖.
提供 一個描述覆蓋圖層的代理方法.
創(chuàng)建一個MKOverlay用于描述畫線.
添加如下方法:
[plain]view plaincopy
private?func?mapRegion()?->?MKCoordinateRegion??{
guard
let?locations?=?run.locations,
locations.count?>?0
else?{
return?nil
}
let?latitudes?=?locations.map?{?location?->?Double?in
let?location?=?location?as!?Location
return?location.latitude
}
let?longitudes?=?locations.map?{?location?->?Double?in
let?location?=?location?as!?Location
return?location.longitude
}
let?maxLat?=?latitudes.max()!
let?minLat?=?latitudes.min()!
let?maxLong?=?longitudes.max()!
let?minLong?=?longitudes.min()!
let?center?=?CLLocationCoordinate2D(latitude:?(minLat?+?maxLat)?/?2,
longitude:?(minLong?+?maxLong)?/?2)
let?span?=?MKCoordinateSpan(latitudeDelta:?(maxLat?-?minLat)?*?1.3,
longitudeDelta:?(maxLong?-?minLong)?*?1.3)
return?MKCoordinateRegion(center:?center,?span:?span)
}
MKCoordinateRegion表示地圖顯示區(qū)域. 通過提供中心點和垂直颈嚼,水平范圍即可確定地圖顯示區(qū)域.
在文件末尾毛秘,大括號之后添加如下擴展:
[plain]view plaincopy
extension?RunDetailsViewController:?MKMapViewDelegate?{
func?mapView(_?mapView:?MKMapView,?rendererFor?overlay:?MKOverlay)?->?MKOverlayRenderer?{
guard?let?polyline?=?overlay?as??MKPolyline?else?{
return?MKOverlayRenderer(overlay:?overlay)
}
let?renderer?=?MKPolylineRenderer(polyline:?polyline)
renderer.strokeColor?=?.black
renderer.lineWidth?=?3
return?renderer
}
}
MapKit每次只能顯示一個覆蓋層. 現(xiàn)在, 如果 覆蓋層 是一個MKPolyine(線段的集合), 返回配置為黑色的 MapKit的MKPolylineRenderer. 接下來將會彩色化這些線段.
最后, 你需要創(chuàng)建一個 overlay.RunDetailsViewController中添加如下方法(不是擴展中):
[plain]view plaincopy
private?func?polyLine()?->?MKPolyline?{
guard?let?locations?=?run.locations?else?{
return?MKPolyline()
}
let?coords:?[CLLocationCoordinate2D]?=?locations.map?{?location?in
let?location?=?location?as!?Location
return?CLLocationCoordinate2D(latitude:?location.latitude,?longitude:?location.longitude)
}
return?MKPolyline(coordinates:?coords,?count:?coords.count)
}
這里, 你需要將跑步位置記錄轉(zhuǎn)換成MKPolyline需求的CLLocationCoordinate2D類型
現(xiàn)在將這些整合到一起. 添加如下方法:
[plain]view plaincopy
private?func?loadMap()?{
guard
let?locations?=?run.locations,
locations.count?>?0,
let?region?=?mapRegion()
else?{
let?alert?=?UIAlertController(title:?"Error",
message:?"Sorry,?this?run?has?no?locations?saved",
preferredStyle:?.alert)
alert.addAction(UIAlertAction(title:?"OK",?style:?.cancel))
present(alert,?animated:?true)
return
}
mapView.setRegion(region,?animated:?true)
mapView.add(polyLine())
}
這里,設(shè)置地圖區(qū)域并且添加覆蓋層.
現(xiàn)在,configureView()方法結(jié)尾添加如下調(diào)用.
[plain]view plaincopy
loadMap()
編譯并運行. 當(dāng)你保存完成的跑步, 你將會看到跑步足跡地圖!
注意:在控制臺, 你將會看到類似以下的錯誤信息:
ERROR /BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1230.34.9.30.27/GeoGL/GeoGL/GLCoreContext.cpp 1763: InfoLog SolidRibbonShader:
ERROR /BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1230.34.9.30.27/GeoGL/GeoGL/GLCoreContext.cpp 1764: WARNING: Output of vertex shader 'v_gradient' not read by fragment shader
/BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1295.30.5.4.13/src/MDFlyoverAvailability.mm:66: Missing latitude in trigger specification
模擬器上這很正常. 這些信息來自 MapKit 阻课,對你來說這并不代表錯誤.
這個應(yīng)用程序已經(jīng)相當(dāng)不錯了叫挟,但是如果你用顏色來區(qū)別速度的差異,地圖可能會更好限煞。
增加一個Cocoa Touch 類文件, 將其命名為MulticolorPolyline作為MKPolyline的子類.
打開MulticolorPolyline.swift導(dǎo)入 MapKit:
[plain]view plaincopy
import?MapKit
類中添加 color 屬性:
[plain]view plaincopy
var?color?=?UIColor.black
哇,就是如此簡單! :] 現(xiàn)在, 難度來了, 打開RunDetailsViewController.swift添加如下方法:
[plain]view plaincopy
private?func?segmentColor(speed:?Double,?midSpeed:?Double,?slowestSpeed:?Double,?fastestSpeed:?Double)?->?UIColor?{
enum?BaseColors?{
static?let?r_red:?CGFloat?=?1
static?let?r_green:?CGFloat?=?20?/?255
static?let?r_blue:?CGFloat?=?44?/?255
static?let?y_red:?CGFloat?=?1
static?let?y_green:?CGFloat?=?215?/?255
static?let?y_blue:?CGFloat?=?0
static?let?g_red:?CGFloat?=?0
static?let?g_green:?CGFloat?=?146?/?255
static?let?g_blue:?CGFloat?=?78?/?255
}
let?red,?green,?blue:?CGFloat
if?speed?<?midSpeed?{
let?ratio?=?CGFloat((speed?-?slowestSpeed)?/?(midSpeed?-?slowestSpeed))
red?=?BaseColors.r_red?+?ratio?*?(BaseColors.y_red?-?BaseColors.r_red)
green?=?BaseColors.r_green?+?ratio?*?(BaseColors.y_green?-?BaseColors.r_green)
blue?=?BaseColors.r_blue?+?ratio?*?(BaseColors.y_blue?-?BaseColors.r_blue)
}?else?{
let?ratio?=?CGFloat((speed?-?midSpeed)?/?(fastestSpeed?-?midSpeed))
red?=?BaseColors.y_red?+?ratio?*?(BaseColors.g_red?-?BaseColors.y_red)
green?=?BaseColors.y_green?+?ratio?*?(BaseColors.g_green?-?BaseColors.y_green)
blue?=?BaseColors.y_blue?+?ratio?*?(BaseColors.g_blue?-?BaseColors.y_blue)
}
return?UIColor(red:?red,?green:?green,?blue:?blue,?alpha:?1)
}
這里, 我們定義了三個基本顏色:紅色抹恳,黃色和綠色. 接著你就可以根據(jù)從最慢到最快的速度范圍生成混合顏色.
將polyLine()中的代碼替換為
[plain]view plaincopy
private?func?polyLine()?->?[MulticolorPolyline]?{
//?1
let?locations?=?run.locations?.array?as!?[Location]
var?coordinates:?[(CLLocation,?CLLocation)]?=?[]
var?speeds:?[Double]?=?[]
var?minSpeed?=?Double.greatestFiniteMagnitude
var?maxSpeed?=?0.0
//?2
for?(first,?second)?in?zip(locations,?locations.dropFirst())?{
let?start?=?CLLocation(latitude:?first.latitude,?longitude:?first.longitude)
let?end?=?CLLocation(latitude:?second.latitude,?longitude:?second.longitude)
coordinates.append((start,?end))
//3
let?distance?=?end.distance(from:?start)
let?time?=?second.timestamp!.timeIntervalSince(first.timestamp!?as?Date)
let?speed?=?time?>?0???distance?/?time?:?0
speeds.append(speed)
minSpeed?=?min(minSpeed,?speed)
maxSpeed?=?max(maxSpeed,?speed)
}
//4
let?midSpeed?=?speeds.reduce(0,?+)?/?Double(speeds.count)
//5
var?segments:?[MulticolorPolyline]?=?[]
for?((start,?end),?speed)?in?zip(coordinates,?speeds)?{
let?coords?=?[start.coordinate,?end.coordinate]
let?segment?=?MulticolorPolyline(coordinates:?coords,?count:?2)
segment.color?=?segmentColor(speed:?speed,
midSpeed:?midSpeed,
slowestSpeed:?minSpeed,
fastestSpeed:?maxSpeed)
segments.append(segment)
}
return?segments
}
以下是新版本的內(nèi)容:
polyline 由線段組成, 每段由端點標(biāo)記. 收集用于描述每段的坐標(biāo)對及每段的速度.
將端點轉(zhuǎn)換成CLLocation對象并成對保存.
計算每段的速度. 注意, Core Location 偶爾會返回時間戳相同的多個更新署驻,所以要防止除以0的錯誤問題. 保存速度并且更新最大和最小速度.
計算整個里程的平均速度.
使用之前計算好的坐標(biāo)對生成新的MulticolorPolyline奋献,并設(shè)置顏色.
在loadMap()中的 mapView.add(polyLine())行, 你將會提示編譯錯誤. 使用下面的代碼替換:
[plain]view plaincopy
mapView.addOverlays(polyLine())
在MKMapViewDelegate擴展中使用如下代碼替換mapView(_:rendererFor:):
[plain]view plaincopy
func?mapView(_?mapView:?MKMapView,?rendererFor?overlay:?MKOverlay)?->?MKOverlayRenderer?{
guard?let?polyline?=?overlay?as??MulticolorPolyline?else?{
return?MKOverlayRenderer(overlay:?overlay)
}
let?renderer?=?MKPolylineRenderer(polyline:?polyline)
renderer.strokeColor?=?polyline.color
renderer.lineWidth?=?3
return?renderer
}
這同之前的版本非常相似.每個覆蓋圖層都是一個MulticolorPolyline并且使用內(nèi)含的顏色渲染線段.
編譯并運行! 讓模擬器啟動慢跑任務(wù)旺上,最后看看彩色地圖!
跑后的地圖是驚人的, 但是如何在跑步期間也展示一個地圖呢?
在 storyboard 中 使用UIStackViews 即可方便添加一個!
首先, 打開NewRunViewController.swift并導(dǎo)入 MapKit:
[plain]view plaincopy
import?MapKit
接著, 打開Main.storyboard并找到New Run View Controller Scene. 確保大綱視圖可見. 如果不可見, 點擊紅框標(biāo)注的按鈕:
向其中拖拽一個UIView并將其放到Top Stack View和Button Stack View之間. 確保其實在他們的之間而不是在任何一個之中. 雙擊它并將其命名為MapContainerView.
在 Attributes Inspector中, 選中Drawing下的Hidden.
在大綱視圖中, Control+拖拽 從Map Container View到Top Stack View同時在彈框中選擇Equal Widths.
拖拽一個MKMapView添加到Map Container View. 按下”Add New Constraints“按鈕 (又名"鈦戰(zhàn)機按鈕") 同時設(shè)置4個約束為 0. 確保 ”Constrain to margins“非選中狀態(tài). 點擊Add 4 Constraints.
大綱視圖中選中Map View, 打開Size Inspector(View\Utilities\Show Size Inspector). 在 constraint區(qū)域雙擊Bottom Space to: Superview.
改變優(yōu)先次序為High (750).
在大綱視圖, Control+拖拽 從Map View到New Run View Controller同時選中delegate.
打開Assistant Editor, 確保是在NewRunViewController.swift并且 從Map ViewControl+拖拽? 創(chuàng)建一個名為mapView的 outlet. 從Map Container ViewControl+拖拽 創(chuàng)建一個名為mapContainerView的outlet.
關(guān)閉Assistant Editor并打開NewRunViewController.swift.
在startRun()頂部添加如下代碼:
[plain]view plaincopy
mapContainerView.isHidden?=?false
mapView.removeOverlays(mapView.overlays)
在 stopRun()頂部 添加如下代碼:
[plain]view plaincopy
mapContainerView.isHidden?=?true
現(xiàn)在, 你需要一個MKMapViewDelegate來進行線段的渲染. 在文件的末尾添加如下擴展:
[plain]view plaincopy
extension?NewRunViewController:?MKMapViewDelegate?{
func?mapView(_?mapView:?MKMapView,?rendererFor?overlay:?MKOverlay)?->?MKOverlayRenderer?{
guard?let?polyline?=?overlay?as??MKPolyline?else?{
return?MKOverlayRenderer(overlay:?overlay)
}
let?renderer?=?MKPolylineRenderer(polyline:?polyline)
renderer.strokeColor?=?.blue
renderer.lineWidth?=?3
return?renderer
}
}
除了線是藍(lán)色瓶蚂,這個同RunDetailsViewController.swift中的代理很像.
最后, 你只需要添加線段圖層并更新地圖區(qū)域,以使地圖顯示區(qū)域為當(dāng)前跑步區(qū)域.在 locationManager(_:didUpdateLocations:)方法中的 代碼distance = distance + Measurement(value: delta, unit: UnitLength.meters)之下添加代碼:
[plain]view plaincopy
let?coordinates?=?[lastLocation.coordinate,?newLocation.coordinate]
mapView.add(MKPolyline(coordinates:?coordinates,?count:?2))
let?region?=?MKCoordinateRegionMakeWithDistance(newLocation.coordinate,?500,?500)
mapView.setRegion(region,?animated:?true)
編譯并運行同時啟動一個跑步任務(wù). 你將會看到實時更新的地圖!
想學(xué)得更快嗎? 觀看視頻以節(jié)省時間
點擊這里下載 截止目前完成功能的項目代碼.
你可能已經(jīng)注意到用戶的速度顯示為"min/mi", 因為本地配置為以米顯示距離 (或者千米).通過調(diào)用 FormatDisplay.pace(distance:seconds:outputUnit:)可以在.minutesPerMile或.minutesPerKilometer進行選擇顯示方式.
繼續(xù)第二部分:如何開發(fā)一款類 Runkeeper 的跑步應(yīng)用之引入徽章成就系統(tǒng).
一如既往, 期待您的意見和問題! :]