iOS-如何开发一款类 Runkeeper 的跑步应用 (下)
2017-07-05 23:51
549 查看
翻译自: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, 添加如下实现代码:
这段代码定义了
在结构体中添加如下属性用于读取和解析JSON:
你可以使用基本的JSON反序列化工具从文件中解析数据并用flatMap过滤掉初始化失败的对象.
你需要进行徽章的比较, 在文件末尾添加如下扩展:
在项目中添加一个文件并命名为: BadgeStatus.swift, 实现代码如下:
此处定义了
本方法将用户的每次跑步任务与取得徽章达到的距离进行比较,从而使每个徽章关联并返回每个获得的徽章的
用户首次获得徽章时,作为参考速度将成为用于确定后续运行是否有足够的提升以获得银版或金版徽章.
最后, 该方法跟踪用户到每个徽章距离的最快速度.
添加一个命名为 BadgeCell.swift 的文件. 替换文件中的代码为:
这些 outlets 将会显示徽章信息. 定义的status变量为cell 的模型.
接着, 在 status 变量 下方添加
这个简单的方法通过设置的BadgeStatus来配置table view cell.
如果你拷贝和粘贴代码, 你会发现Xcode 会把
将会显示一个简单的拾色器. 点击 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部分替换为import
接着, 添加类定义:
当视图加载时, 从Core Data 中读取已经完成的跑步任务列表, 按时间排序, 接着使用它创建获取的徽章列表.
下一步, 在扩展中添加
这些标准的
编译并运行来获取你的新徽章! 你将会看到如下样子的界面:
这些outlets 用于UI页面展示控制,
接着, 添加
从
金版和银版徽章图像如果需要隐藏时可通过设置alphas 为 0.
最后, 添加如下方法:
当按下info按钮是此方法就会被调用同时显示一个 带有徽章信息的弹出窗口.
打开 Main.storyboard. 将
badgeImageView
nameLabel
distanceLabel
earnedLabel
bestLabel
silverLabel
goldLabel
silverImageView
goldImageView
info按钮 关联响应事件:
Attributes Inspector 中 选中 User Interaction Enabled :
打开BadgesTableViewController.swift 并添加如下扩展:
当用户在列表中按下一个徽章,它负责将
iOS 11 注意: 当前 beta 版本的 iOS 11在cell配置后或者显示之前会把
编译并运行. 获取徽章详情!
打开 Badge.swift 并添加如下方法:
这些方法都会根据已经获得或尚未获得徽章来过滤徽章列表.
现在, 打开 Main.storyboard. 在 New Run View Controller Scene 找到
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并命名为:
Xcode 9 注意: 如果你发现一对由新控件的垂直位置因歧义引起的警告, 请不要担心. 你的Xcode版本没有正确计算隐藏子视图的布局. 要想消除警告, 在Main.storyboard 的
如果一切顺利的话, 本问题将在Xcode 9的发布版中解决.
打开 NewRunViewController.swift 并导入 AVFoundation:
现在, 添加如下属性:
接着, 找到
这个用于更新即将获得的徽章.
在
这个会展示将要获得的徽章.
在
如同其他视图中一样, 所有的徽章在跑步期间应该被隐藏.
添加如下新方法:
该方法用于检查何时会获得, 更新UI并展示下一个徽章, 同时播放一个胜利声音庆祝获得一个徽章.
在
编译并运行,随着模拟器模拟跑步观察标签的更新. 当获得一个徽章时听播放的声音!
注意: 在控制台中, 一旦播放 成功的声音, 你将会看到如下的错误信息:
模拟器上这是正常的. 这个信息来自 AVFoundation,对于你来说这并不算是个错误.
同样, 如果你不想呆呆的等着测试徽章, 你可以在模拟器的Debug\Location菜单切换到不同的位置模式. 别担心, 我们不会告诉任何人. :]
打开 Main.storyboard 找到 Run Details View Controller Scene. 在 Map View顶层拖拽一个
将Image View 边关联到 Map View边.
Xcode 将会添加约束, 每个的值为0. 然而目前, Image View 并没有完全覆盖 Map View,因此你会看到黄色警告线. 点击Update Frames 按钮 (底部红色框标注) 来调整 Image View的大小.
在Image View上拖拽一个
从 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 属性 因为这样将会启动动画效果让用户获得更加流畅的用户体验.
在视图的后下角添加一个
选中Switch并按下 Add New Contraints 按钮 (钛战机按钮). 添加约束Right,Bottom 和Left 设置值为8. 确保Left 约束相对于Label. 选择Add 3 Constraints.
Set the Switch Value to Off.
从 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关联:
为Switch 添加 事件响应:
当 switch 值改变, 你可以通过改变
现在,为Info Button 添加响应事件:
这同BadgeDetailsViewController.swift中的按钮响应事件类似.
最后一步是在
当用户跑步的时候你可以找到用户获得的最新的徽章并展示出来.
编译并运行. 在模拟器上启动跑步, 保存信息并尝试你的“太空模式”!
MapKit 使用 annotations 来展示数据点. 要想创建annotations, 你需要:
一个遵守
一个
你需要实现这些:
创建类
创建一个
实现
添加一个文件命名为 BadgeAnnotation.swift. 替换代码如下:
打开 RunDetailsViewController.swift 并添加如下新方法:
这段代码创建了一个存储
在
这行代码将annotations添加到地图上.
最后, 添加如下扩展:
这里, 你为每个annotation创建一个
编译并运行. 在模拟器上启动一个跑步任务并在最后保存跑步信息. 地图上将会展示每个获得的徽章的annotation. 点击一个你将会看到 名称, 图片 和 距离.
在这个包含两部分的教程中 ,你开发了一个应用程序:
使用Core Location 度量和跟踪你的跑步任务.
显示实时数据, 如跑步的平均速度,还有一张动态地图.
在地图上展示彩色编码的线段并自定义每个检查点的annotation.
基于距离和速度的个人进程的奖励徽章.
还有很多功能需要你去实现:
为用户添加历史跑步列表.
计算每两个检查点之间的平均速度并在
感谢您的阅读. 一如既往, 期待您的意见和问题! :]
更新提醒: 本教程已由 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, 添加如下实现代码:
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:
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是为了保证损耗性能的解析操作只执行一次.
你需要进行徽章的比较, 在文件末尾添加如下扩展:
extension Badge: Equatable { static func ==(lhs: Badge, rhs: Badge) -> Bool { return lhs.name == rhs.name } }
赢得徽章
现在已经创建了Badge结构体, 你需要一个结构体来存储已经获得的徽章. 此结构体将Badge和各种Run对象(如果有的话)关联起来, 用户可以读取已经奖励的徽章的版本.
在项目中添加一个文件并命名为: BadgeStatus.swift, 实现代码如下:
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结构体 和 用户提高多少时间获取银版或者金版徽章的乘数. 接着在结构体中添加如下方法:
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 的文件. 替换文件中的代码为:
importUIKit 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()方法:
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部分替换为import
UIKit和
CoreData:
importUIKit
import CoreData
接着, 添加类定义:
class BadgesTableViewController: UITableViewController { var statusList: [BadgeStatus]! override func viewDidLoad() { super.viewDidLoad() statusList = BadgeStatus.badgesEarned(runs: getRuns()) } private func getRuns() -> [Run] { let fetchRequest: NSFetchRequest<Run> = 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
方法:
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的文件. 使用如下代码替换文件内容:importUIKit
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():
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()方法末尾添加如下代码:
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.
最后, 添加如下方法:
@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 并添加如下扩展:
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 并添加如下方法:
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并命名为:
@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()中添加如下代码:
badgeStackView.isHidden = true // required to work around behavior change in Xcode 9 beta 1
如果一切顺利的话, 本问题将在Xcode 9的发布版中解决.
打开 NewRunViewController.swift 并导入 AVFoundation:
import AVFoundation
现在, 添加如下属性:
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()添加如下代码:
let distanceRemaining = upcomingBadge.distance - distance.value let formattedDistanceRemaining = FormatDisplay.distance(distanceRemaining) badgeInfoLabel.text = "\(formattedDistanceRemaining) until \(upcomingBadge.name)"
这个用于更新即将获得的徽章.
在
startRun(), 调用
updateDisplay()之前, 添加:
badgeStackView.isHidden = false upcomingBadge = Badge.next(for: 0) badgeImageView.image = UIImage(named: upcomingBadge.imageName)
这个会展示将要获得的徽章.
在
stopRun()中 添加:
badgeStackView.isHidden = true
如同其他视图中一样, 所有的徽章在跑步期间应该被隐藏.
添加如下新方法:
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()中 在 调用to
updateDisplay():之前 添加对
checkNextBadge()的调用
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 Switch Value to Off.
从 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关联:
@IBOutlet weak var badgeImageView: UIImageView! @IBOutlet weak var badgeInfoButton: UIButton!
为Switch 添加 事件响应:
@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 添加响应事件:
@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()方法末尾添加如下代码:
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. 替换代码如下:
import MapKit class BadgeAnnotation: MKPointAnnotation { let imageName: String init(imageName: String) { self.imageName = imageName super.init() } }
MKPointAnnotation遵守
MKAnnotation协议,你需要一种方式为渲染系统传入图片名字.
打开 RunDetailsViewController.swift 并添加如下新方法:
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()末尾添加如下代码:
mapView.addAnnotations(annotations())
这行代码将annotations添加到地图上.
最后, 添加如下扩展:
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进行展示.
感谢您的阅读. 一如既往, 期待您的意见和问题! :]
相关文章推荐
- iOS-如何开发一款类 Runkeeper 的跑步应用 (上)
- 如何开发出成功的iOS应用(流程图)
- Flash开发iOS应用全攻略(四)——如何为iTunes Connect准备应用
- Flash开发iOS应用全攻略(四)——如何为iTunes Connect准备应用
- 如何开发出成功的iOS应用(流程图)
- TUP第30期:微软资深专家论如何用Visual Studio开发iOS、Android应用
- 【iOS越狱开发】如何将应用打包成.ipa文件
- Flash开发iOS应用全攻略(三)——如何使用iOS开发者授权以及如何申请证书
- 【iOS越狱开发】如何将应用打包成.ipa文件
- Flash开发iOS应用全攻略(四)——如何为iTunes Connect准备应用
- iOS开发经验谈:如何提高应用开发效率?
- Flash开发iOS应用全攻略(四)——如何为iTunes Connect准备应用
- Flash开发iOS应用全攻略(三)——如何使用iOS开发者授权以及如何申请证书
- iOS开发经验谈:如何提高应用开发效率?
- Flash开发iOS应用全攻略(五)——如何上传应用到iTunes Connect
- Flash开发iOS应用全攻略(五)——如何上传应用到iTunes Connect
- Flash开发iOS应用全攻略(五)——如何上传应用到iTunes Connect
- ArcGIS Runtime SDK for iOS开发系列教程(4)——如何让你的iOS应用具有GIS能力
- 如何开发出成功的iOS应用(流程图)
- iOS 如何将应用装入开发用的设备中测试 (未越狱)