iOS-如何開發(fā)一款類 Runkeeper 的跑步應(yīng)用 (上)

翻譯自: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)該熟悉StoryboardsCore Data. 如果您絕得需要復(fù)習(xí)下知識,請查閱鏈接教程.

本教程同時也使用了iOS10中新增加的MeasurementMeasurementFormatter功能. 更多了解請觀看視頻.

鑒于內(nèi)容眾多,本教程將分為兩部分. 第一部分重點講解 記錄跑步數(shù)據(jù)和地圖路線展示. 第二部分介紹了徽章系統(tǒng).

開始

下載項目模板. 其中包括要完成本教程的所有文件和資源.

花費幾分鐘熟悉下項目.Main.storyboard已經(jīng)包含了 所有UI界面. 將AppDelegate中關(guān)于Core Data的模板代碼移到CoreDataStack.Swift中.Assets.xcassets中包含了將要使用的圖片和聲音文件.

模型: Runs 和 Locations

MoonRunner 使用 Core Data 相對簡單, 僅僅使用了兩個實體:Run和Location.

打開MoonRunner.xcdatamodeld同時創(chuàng)建兩個實體:RunLocation. 在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代碼.

完成基本的應(yīng)用流程

打開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).

啟動一個跑步任務(wù)

基本上可以開始跑步了. 但是首先, 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用于每秒更新一次并收集位置更新.

保存跑步數(shù)據(jù)

某一時刻, 你的用戶感覺累了并停止跑步. 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ù)旺上,最后看看彩色地圖!

如何實時導(dǎo)航?

跑后的地圖是驚人的, 但是如何在跑步期間也展示一個地圖呢?

在 storyboard 中 使用UIStackViews 即可方便添加一個!

首先, 打開NewRunViewController.swift并導(dǎo)入 MapKit:

[plain]view plaincopy

import?MapKit

接著, 打開Main.storyboard并找到New Run View Controller Scene. 確保大綱視圖可見. 如果不可見, 點擊紅框標(biāo)注的按鈕:

向其中拖拽一個UIView并將其放到Top Stack ViewButton Stack View之間. 確保其實在他們的之間而不是在任何一個之中. 雙擊它并將其命名為MapContainerView.

在 Attributes Inspector中, 選中Drawing下的Hidden.

在大綱視圖中, Control+拖拽 從Map Container ViewTop 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 ViewNew 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).

一如既往, 期待您的意見和問題! :]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宣吱,一起剝皮案震驚了整個濱河市窃这,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌征候,老刑警劉巖杭攻,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洒试,死亡現(xiàn)場離奇詭異,居然都是意外死亡朴上,警方通過查閱死者的電腦和手機垒棋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痪宰,“玉大人叼架,你說我怎么就攤上這事∫虑耍” “怎么了乖订?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長具练。 經(jīng)常有香客問我乍构,道長,這世上最難降的妖魔是什么扛点? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任哥遮,我火速辦了婚禮,結(jié)果婚禮上陵究,老公的妹妹穿的比我還像新娘眠饮。我一直安慰自己,他們只是感情好铜邮,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布仪召。 她就那樣靜靜地躺著,像睡著了一般松蒜。 火紅的嫁衣襯著肌膚如雪扔茅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天秸苗,我揣著相機與錄音召娜,去河邊找鬼。 笑死难述,一個胖子當(dāng)著我的面吹牛萤晴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胁后,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼店读,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了攀芯?” 一聲冷哼從身側(cè)響起屯断,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后殖演,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氧秘,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年趴久,在試婚紗的時候發(fā)現(xiàn)自己被綠了丸相。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡彼棍,死狀恐怖灭忠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情座硕,我是刑警寧澤弛作,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站华匾,受9級特大地震影響映琳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜘拉,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一萨西、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诸尽,春花似錦原杂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽年局。三九已至际看,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間矢否,已是汗流浹背仲闽。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留僵朗,地道東北人赖欣。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像验庙,于是被迫代替她去往敵國和親顶吮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容