版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.18 星期四 |
前言
很多的app都有定位功能,比如说滴滴,美团等,他们都需要获取客户所在的位置,并且根据位置推送不同的模块数据以及服务,可以说,定位方便了我们的生活,接下来这几篇我们就说一下定位框架
CoreLocation
。感兴趣的可以看我写的上面几篇。
1. CoreLocation框架详细解析 —— 基本概览(一)
2. CoreLocation框架详细解析 —— 选择定位服务的授权级别(二)
3. CoreLocation框架详细解析 —— 确定定位服务的可用性(三)
4. CoreLocation框架详细解析 —— 获取用户位置(四)
5. CoreLocation框架详细解析 —— 监控用户与地理区域的距离(五)
6. CoreLocation框架详细解析 —— 确定接近iBeacon(六)
7. CoreLocation框架详细解析 —— 将iOS设备转换为iBeacon(七)
8. CoreLocation框架详细解析 —— 获取指向和路线信息(八)
9. CoreLocation框架详细解析 —— 在坐标和用户友好的地名之间转换(九)
10. CoreLocation框架详细解析(十) —— 跟踪访问位置简单示例(一)
11. CoreLocation框架详细解析(十一) —— 跟踪访问位置简单示例(二)
12. CoreLocation框架详细解析(十二) —— 仿Runkeeper的简单实现(一)
13. CoreLocation框架详细解析(十三) —— 仿Runkeeper的简单实现(二)
开始
首先看一下写作环境
Swift 4, iOS 11, Xcode 9
该应用程序在当前状态下非常适合记录和显示数据,但它需要更多的火花才能为用户提供额外的动力。
在本节中,您将通过实施徽章系统来完成演示MoonRunner
应用程序,该系统体现了健身是一种有趣且基于进步的成就的概念。 以下是它的工作原理:
- 列表列出了增加距离的检查点以激励用户。
- 当用户跑步时,该应用程序会显示即将到来的徽章的缩略图以及获得它的剩余距离。
- 当用户第一次到达检查点时,应用程序会颁发徽章并记录运行的平均速度。从那里开始,徽章的银色和金色版本将以相对更快的速度再次到达该检查站。
- 跑步后的地图在路径上的每个检查点显示一个点,其中自定义标注显示徽章名称和图像。
无论您使用哪个文件,您都会注意到您的项目包含资产目录中的许多图像以及名为badges.txt
的文件。 现在打开badges.txt
。 您可以看到它包含一个大型JSON徽章对象数组。 每个对象包含:
- 一个名字
name
。 - 有关徽章的一些有趣信息
information
。 - 以米为单位实现徽章的距离
distance
。 - 资产目录
(imageName)
中相应图像的名称。
徽章从0米开始 - 嘿,你必须从某个地方开始 - 直到完整马拉松的长度。
第一项任务是将JSON文本解析为徽章数组。 将新的Swift文件添加到项目中,将其命名为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
,因此昂贵的解析操作只发生一次。
您需要以后能够匹配Badge
,因此请将以下扩展添加到文件末尾:
extension Badge: Equatable {
static func ==(lhs: Badge, rhs: Badge) -> Bool {
return lhs.name == rhs.name
}
}
Earning The Badge - 赢得徽章
现在您已经创建了Badge
结构,在获得徽章时,您需要一个存储结构。 此结构将徽章与用户获得此Badge
版本的各种Run
对象(如果有)相关联。
将新的Swift文件添加到项目中,将其命名为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
值。
用户第一次获得徽章时,该跑步速度成为用于确定后续跑步是否已经足够改进以获得银色或金色版本的参考。
最后,该方法跟踪用户对每个徽章距离的最快跑步。
Displaying the Badges - 显示徽章
现在您已经将所有逻辑写入授予徽章,现在是时候向用户显示它们了。 初始项目已经定义了必要的UI。 您将在UITableViewController
中显示徽章列表。 为此,首先需要定义显示徽章的自定义table view cell
。
将新的Swift文件添加到项目中并将其命名为BadgeCell.swift
。 用以下内容替换文件的内容:
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
变量,它是单元格的模型。
接下来,在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
配置表视图单元格。
如果复制并粘贴代码,您会注意到Xcode将#colorLiterals
更改为样本。 如果您手动输入,请开始键入单词Color literal
,选择Xcode完成并双击生成的样本。
这将显示一个简单的颜色选择器。 单击Other…
按钮。
这将调出系统颜色选择器。 要匹配示例项目中使用的颜色,请使用Hex Color#
字段,输入FF142C
表示红色,输入00924E
表示绿色。
打开Main.storyboard
并将outlets
连接到Badges Table View Controller Scene
中的BadgeCell
:
badgeImageView
silverImageView
goldImageView
nameLabel
earnedLabel
现在已经定义了table cell
,现在是时候创建table view controller
了。 将新的Swift文件添加到项目中并将其命名为BadgesTableViewController.swift
。 替换导入部分以导入UIKit
和CoreData
:
import UIKit
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
}
}
这些是所有UITableViewControllers
所需的标准UITableViewDataSource
方法,将行数和已配置的单元格返回到表中。 正如在第1部分中一样,您通过在StoryboardSupport.swift
中定义的泛型方法使单元格出列来减少“stringly typed”
代码。
Build并运行以检查您的新徽章! 你应该看到这样的东西:
What Does a Runner Have to Do to Get a Gold Medal Around Here? - 跑步者必须做些什么来获得这里的金牌?
MoonRunner
的最后一个视图控制器是显示徽章细节的控制器。 将新的Swift文件添加到项目中并将其命名为BadgeDetailsViewController.swift
。 用以下内容替换文件的内容:
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!
}
这将声明控制UI所需的所有outlets
以及作为此视图模型的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!"
}
必要时通过将其alpha
设置为0来隐藏金色和银色图像视图。这适用于嵌套的UIStackViews
和自动布局之间的交互。
最后,添加以下方法:
@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)
}
按下信息按钮时将调用此选项,并显示带有徽章信息的弹出窗口。
打开Main.storyboard
。 连接BadgeDetailsViewController
的outlets
:
badgeImageView
nameLabel
distanceLabel
earnedLabel
bestLabel
silverLabel
goldLabel
silverImageLabel
goldImageLabel
将操作infoButtonTapped()
连接到info button
。 最后,在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注意:iOS 11的当前测试版在配置单元格之后和显示之前将表格单元格的属性
isUserInteractionEnabled
重置为true
。 因此,您必须实现shouldPerformSegue(withIdentifier:sender :)
以防止访问未获得的徽章的徽章详细信息。 如果更高版本的iOS 11更正此错误,则可以删除此方法。
建立并运行。 查看您的新徽章的详细信息!
Carrot Motivation - 激励
现在您已经拥有了一个很酷的新徽章系统,您需要更新现有应用程序的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拖到Document Outline
中。 确保它们位于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
使用您最喜欢的Assistant Editor连接新的Stack View, Image View and Label
中的outlets
,命名如下:
@IBOutlet weak var badgeStackView: UIStackView!
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var badgeInfoLabel: UILabel!
Xcode 9注意:如果你看到一对警告说你的新UI项目的垂直位置不明确,请不要担心。 您的Xcode版本无法正确计算隐藏项目的子视图的布局。 要使警告消失,请取消选中
Main.storyboard
中徽章堆栈视图上的Hidden
属性。 然后将以下行添加到NewRunViewController.swift
中的viewDidLoad()
:
badgeStackView.isHidden = true // required to work around behavior change in Xcode 9 beta 1
打开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
是作为“success sound”
的音频播放器创建的,每次获得新徽章时都会播放该声音播放器。
接下来,找到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()
中,在调用updateDisplay()
之前添加对checkNextBadge()
的调用:
checkNextBadge()
构建并运行以在模拟器运行时观察标签更新。 通过新徽章时听取声音!
注意:在控制台中,一旦播放成功声音,您可能会看到一些如下所示的错误消息:
[aqme] 254: AQDefaultDevice (188): skipping input stream 0 0 0x0
在模拟器上,这是正常的。 消息来自AVFoundation
,并不表示您的错误。
此外,如果您不想等待测试徽章,您可以随时在模拟器的Debug \ Location
菜单中切换到其他位置模式。
后记
本篇主要讲述了仿Runkeeper的简单实现,感兴趣的给个赞或者关注~~~