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

翻譯自: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 追蹤路線.

地圖上顯示路徑及記錄跑步時的平均速度.

當(dāng)跑步結(jié)束后顯示一個路線地圖. 不同顏色的線段表示不同的速度.

當(dāng)前完成的應(yīng)用,足以記錄和顯示數(shù)據(jù), 但要激勵用戶跑步需要更多的鼓勵.

在本部分, 你將會通過實現(xiàn)徽章系統(tǒng)來完成MoonRunner應(yīng)用,這個徽章系統(tǒng)體現(xiàn)了健身是一種樂趣以及進(jìn)步的成就.

功能如下:

地圖上標(biāo)出距離增長的檢查點的列表用于激勵用戶.

當(dāng)用戶跑步時,app顯示即將獎勵的徽章縮略圖及獲取徽章所需剩余距離.

用戶首次到達(dá)檢查點, app 獎勵 徽章并記錄跑步的平均速度.

從那里開始, 以更快速度到達(dá)檢查點將再次被授予銀版和金版徽章.

跑后地圖會沿著路徑在每一個檢查點顯示一個圓點,點擊圓點可以展示徽章的名字和圖片.

開始

假如你已經(jīng)完成了教程的第一部分, 你可以在第一部分的項目基礎(chǔ)上繼續(xù)項目開發(fā). 如果你直接從部分開始, 下載本部分項目模板.

不管你使用什么文件, 你將會注意到你的項目包括asset下的圖片文件和一個badges.txt文件. 現(xiàn)在打開badges.txt.? 文件中包括徽章對象的JSON數(shù)組. 每個對象包括:

名稱.

徽章的一些有用信息.

獲得徽章的距離, 以米為單位.

在asset目錄中對應(yīng)的圖片文件名.

徽章記錄從 0 米開始 — 嘿, 你必須從某個地方開始 — 直到整個馬拉松結(jié)束.

第一個任務(wù)就是將JSON文本解析為徽章對象數(shù)組. 項目中添加一個文件并命名為Badge.Swift, 添加如下實現(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)并提供了一個從JSON對象提取信息的 可返回失敗構(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過濾掉初始化失敗的對象.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)體, 你需要一個結(jié)構(gòu)體來存儲已經(jīng)獲得的徽章. 此結(jié)構(gòu)體將Badge和各種Run對象(如果有的話)關(guān)聯(lián)起來, 用戶可以讀取已經(jīng)獎勵的徽章的版本.

在項目中添加一個文件并命名為: BadgeStatus.swift, 實現(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ù). 接著在結(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)行比較,從而使每個徽章關(guān)聯(lián)并返回每個獲得的徽章的BadgeStatus數(shù)組.

用戶首次獲得徽章時,作為參考速度將成為用于確定后續(xù)運(yùn)行是否有足夠的提升以獲得銀版或金版徽章.

最后, 該方法跟蹤用戶到每個徽章距離的最快速度.

徽章顯示

到目前為止,你已經(jīng)實現(xiàn)了獲取徽章獎勵的邏輯, 現(xiàn)在向用戶展示他們. 項目模板中已經(jīng)定義了必須的UI界面. 你需要在一個UITableViewController顯示徽章列表. 要想顯示內(nèi)容, 首先,你需要自定義顯示徽章的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

}

}

這個簡單的方法通過設(shè)置的BadgeStatus來配置table view cell.

如果你拷貝和粘貼代碼, 你會發(fā)現(xiàn)Xcode 會把#colorLiterals 轉(zhuǎn)變?yōu)轭伾甘緣K. 如果你是打字輸入, 鍵入單詞Color literal, 選中完成并雙擊顏色指示塊.

將會顯示一個簡單的拾色器. 點擊Other…按鈕.

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

打開Main.storyboard同時 在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.? 在項目中添加一個命名為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)視圖加載時, 從Core Data 中讀取已經(jīng)完成的跑步任務(wù)列表, 按時間排序, 接著使用它創(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)行來獲取你的新徽章! 你將會看到如下樣子的界面:

為了獲得金牌跑步者應(yīng)該做些什么?

MoonRunner最后一個頁面是展示徽章詳細(xì)信息的. 項目中添加一個命名為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頁面展示控制,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ì)頁面中的標(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è)置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按鈕是此方法就會被調(diào)用同時顯示一個 帶有徽章信息的彈出窗口.

打開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)用戶在列表中按下一個徽章,它負(fù)責(zé)將BadgeStatus傳遞給BadgeDetailsViewController.

iOS 11 注意:當(dāng)前 beta 版本的 iOS 11在cell配置后或者顯示之前會把UserInteractionEnabled重置為true. 因此,你必須實現(xiàn)shouldPerformSegue(withIdentifier:sender:)來防止訪問未獲得的徽章的詳細(xì)信息. 如果iOS11的后續(xù)版本修復(fù)此bug, 這個方法可以刪除掉.

編譯并運(yùn)行. 獲取徽章詳情!

胡蘿卜式激勵

現(xiàn)在,你已經(jīng)實現(xiàn)了一個很酷的徽章系統(tǒng), 你需要將其融合到現(xiàn)有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!

}

這些方法都會根據(jù)已經(jīng)獲得或尚未獲得徽章來過濾徽章列表.

現(xiàn)在, 打開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

設(shè)置圖像的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 去關(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)一對由新控件的垂直位置因歧義引起的警告, 請不要擔(dān)心. 你的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的發(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)每次獲得一個新徽章,successSound是 一個音頻播放器 用于 播放? "成功聲音"

接著, 找到updateDisplay()添加如下代碼:

[plain]view plaincopy

let?distanceRemaining?=?upcomingBadge.distance?-?distance.value

let?formattedDistanceRemaining?=?FormatDisplay.distance(distanceRemaining)

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

這個用于更新即將獲得的徽章.

在startRun(), 調(diào)用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

如同其他視圖中一樣, 所有的徽章在跑步期間應(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)

}

}

該方法用于檢查何時會獲得, 更新UI并展示下一個徽章, 同時播放一個勝利聲音慶祝獲得一個徽章.

在eachSecond()中 在 調(diào)用toupdateDisplay():之前 添加對checkNextBadge()的調(diào)用

[plain]view plaincopy

checkNextBadge()

編譯并運(yùn)行,隨著模擬器模擬跑步觀察標(biāo)簽的更新. 當(dāng)獲得一個徽章時聽播放的聲音!

注意:在控制臺中, 一旦播放 成功的聲音, 你將會看到如下的錯誤信息:

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

模擬器上這是正常的. 這個信息來自 AVFoundation,對于你來說這并不算是個錯誤.

同樣, 如果你不想呆呆的等著測試徽章, 你可以在模擬器的Debug\Location菜單切換到不同的位置模式. 別擔(dān)心, 我們不會告訴任何人. :]

當(dāng)存在一個“空間模式”時,一切都會變得更好

在一個跑步任務(wù)結(jié)束后, 提供一個讓用戶可以看到最新獲得的徽章的功能會更好.

打開Main.storyboard找到Run Details View Controller Scene. 在 Map View頂層拖拽一個UIImageView. 從Image View Control+拖拽 到 Map View. 在彈出窗口中,選擇Top,Bottom,LeadingTrailing. 點擊Add Constraints將Image View 邊關(guān)聯(lián)到 Map View邊.

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

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

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

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

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

選中 Image View 并設(shè)置Content ModeAspect Fit,Alpha0.

選中 按鈕設(shè)置Alpha0.

注意:你應(yīng)該使用Alpha 屬性來隱藏這些視圖而不是使用 Hidden 屬性 因為這樣將會啟動動畫效果讓用戶獲得更加流暢的用戶體驗.

在視圖的后下角添加一個UISwitch和一個UILabel.

選中Switch并按下Add New Contraints按鈕 (鈦戰(zhàn)機(jī)按鈕). 添加約束Right,BottomLeft設(shè)置值為8. 確保Left約束相對于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.

喲! 在所有的布局工作完成之后你獲得一個徽章! :]

在 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)用戶跑步的時候你可以找到用戶獲得的最新的徽章并展示出來.

編譯并運(yùn)行. 在模擬器上啟動跑步, 保存信息并嘗試你的“太空模式”!

在你的地圖上展示徽章

跑后的地圖已經(jīng)幫助你記錄你的路線和展示速度較慢的區(qū)域. 現(xiàn)在 你將要添加一個功能:精確的展示每個徽章是從哪里獲得的.

MapKit 使用 annotations 來展示數(shù)據(jù)點. 要想創(chuàng)建annotations, 你需要:

一個遵守MKAnnotation協(xié)議的類,用于提供描述annotation位置的坐標(biāo).

一個MKAnnotationView的子類用于顯示關(guān)聯(lián)annotation的信息.

你需要實現(xiàn)這些:

創(chuàng)建類BadgeAnnotation其遵守MKAnnotation協(xié)議.

創(chuàng)建一個存儲BadgeAnnotation對象的數(shù)組并將其添加到地圖上.

實現(xiàn)mapView(_:viewFor:)用戶創(chuàng)建MKAnnotationViews.

添加一個文件命名為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)建了一個存儲BadgeAnnotation對象的數(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

}

這里, 你為每個annotation創(chuàng)建一個MKAnnotationView并設(shè)置顯示徽章的圖像.

編譯并運(yùn)行. 在模擬器上啟動一個跑步任務(wù)并在最后保存跑步信息. 地圖上將會展示每個獲得的徽章的annotation. 點擊一個你將會看到 名稱, 圖片 和 距離.

下一步

你可以下載到本教程的完整項目示例代碼.

在這個包含兩部分的教程中 ,你開發(fā)了一個應(yīng)用程序:

使用Core Location 度量和跟蹤你的跑步任務(wù).

顯示實時數(shù)據(jù), 如跑步的平均速度,還有一張動態(tài)地圖.

在地圖上展示彩色編碼的線段并自定義每個檢查點的annotation.

基于距離和速度的個人進(jìn)程的獎勵徽章.

還有很多功能需要你去實現(xiàn):

為用戶添加歷史跑步列表.NSFetchedResultsController和 現(xiàn)有的RunDetailsViewController使這成為小菜一碟!

計算每兩個檢查點之間的平均速度并在MKAnnotationView進(jìn)行展示.

感謝您的閱讀. 一如既往, 期待您的意見和問題! :]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容