版本记录
版本号 | 时间 |
---|---|
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框架详细解析(十一) —— 跟踪访问位置简单示例(二)
开始
首先看一下写作环境
Swift 4, iOS 11, Xcode 9
Runkeeper
是一款GPS应用程序,就像您即将制作的那样,拥有超过4000万用户! 本教程将向您展示如何制作像Runkeeper这样的应用程序。
运动跟踪应用程序Runkeeper拥有超过4000万用户!本教程将向您展示如何制作像Runkeeper这样的应用程序,它将教您如下:
- 使用
Core Location
来跟踪您的路线。 - 在跑步期间显示地图,并标记路径的不断更新的线。
- 报告您跑步时的平均速度。
- 奖励跑步各种距离的徽章。无论您的起点如何,每个徽章的银色和金色版本都能识别个人进步。
- 通过跟踪到下一个徽章的剩余距离来鼓励您。
- 完成后显示路线地图。地图线采用颜色编码,以反映您的步伐。
你的新应用程序 - MoonRunner
- 在我们的太阳系中使用基于行星和卫星的徽章!
在您深入学习本教程之前,您应该熟悉Storyboard
和Core Data
。
这个教程也使用iOS 10的新Measurement
和MeasurementFormatter
功能。
有很多内容可以讨论,本教程分为两部分。第一部分重点是记录运行数据并渲染颜色编码的地图。第二部分介绍了徽章系统。
打开入门项目。 它包含完成本教程所需的所有项目文件和资产。
花几分钟时间来探索这个项目。 Main.storyboard
已包含UI。 CoreDataStack.swift
从AppDelegate
中删除Apple的模板Core Data代码并将其放在自己的类中。Assets.xcassets
包含您将使用的图像和声音。
Model: Runs and Locations - 模型:跑步和位置
MoonRunner
使用Core Data
非常简单,只使用两个实体:Run
和Location
。
打开MoonRunner.xcdatamodeld
并创建两个实体:Run
和Location
。 使用以下属性配置Run
:
Run
有三个属性:distance
,duration
和timestamp
。 它有一个关系,即locations
,将它连接到Location
实体。
注意:在下一步之后,您将无法设置反向关系。 这会引起警告。 别恐慌!
现在,使用以下属性设置Location
:
Location
还有三个属性:latitude
, longitude
和 timestamp
以及单个关系,run
。
选择Run
实体并验证其locations
关系Inverse
属性现在显示为“run”
。
选择locations
关系,并将Type
设置为To Many
,然后选中Data Model Inspector’s Relationship
窗格中的Ordered
框。
最后,验证Run
和Location
实体的Codegen
属性是否在Data Model Inspector
的Entity
窗格中设置为Class Definition
(这是默认值)。
构建项目,以便Xcode可以为您的Core Data模型生成必要的Swift定义。
Completing the Basic App Flow - 完成基本应用程序流程
打开RunDetailsViewController.swift
并在viewDidLoad()
之前添加以下行:
var run: Run!
接下来,在viewDidLoad中()
之后添加以下函数:
private func configureView() {
}
最后,在调用super.viewDidLoad()
之后的viewDidLoad()
内部,添加对configureView()
的调用。
configureView()
这设置了在应用程序中完成导航所需的最低限度。
打开NewRunViewController.swift
并在viewDidLoad()
之前添加以下行:
private var run: Run?
接下来,添加以下新方法:
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
隐藏在故事板中。 这些例程在“未运行”和“运行期间”模式之间切换UI。
在startTapped()
中,添加对startRun()
的调用。
startRun()
在文件的末尾,在结束括号后,添加以下扩展名:
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
}
}
}
Apple的故事板segues
接口通常被称为“stringly typed”
。 segue标识符是一个字符串,没有错误检查。 利用Swift协议和枚举的强大功能,以及StoryboardSupport.swift
中的一点点小精灵,您可以避免这种“stringly typed”
界面的大部分痛苦。
接下来,将以下行添加到stopTapped()
:
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)
当用户按下停止按钮时,您应该让他们决定是保存,丢弃还是继续跑。 您使用UIAlertController
来提示用户并获得他们的响应。
Build并运行。 按New Run
按钮,然后按Start
按钮。 验证UI是否更改为“running mode”
:
按Stop
按钮,确认按Save
将您带到“详细信息”屏幕。
注意:在控制台中,您可能会看到一些如下所示的错误消息:
MoonRunner[5400:226999] [VKDefault] /BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1295.30.5.4.13/src/MDFlyoverAvailability.mm:66: Missing latitude in trigger specification
这是正常现象,并不表示您的错误。
Units and Formatting - 单位和格式
iOS 10引入了新功能,使得使用和显示测量单位变得更加容易。 跑步者倾向于考虑他们在步速pace
(每单位距离的时间)方面的进步,这是速度speed
的反转(每单位时间的距离)。 您必须扩展UnitSpeed
以支持步伐pace
的概念。
将新的Swift文件添加到名为UnitExtensions.swift
的项目中。 在import
语句后添加以下内容:
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
以进行pace
测量之间转换之前,必须创建一个可以处理数学运算的UnitConverter
。 子类化UnitConverter
要求您实现baseUnitValue(fromValue :)
和value(fromBaseUnitValue :)
。
现在,将此代码添加到文件的末尾:
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
中提供的众多Unit
之一。 UnitSpeed
的默认单位是“米/秒”。 您的扩展允许以minutes/km
或者 minutes/mile
表示速度。
您需要一种统一的方式来显示MoonRunner
中的距离,时间,步速和日期等数量。 MeasurementFormatter
和DateFormatter
使这很简单。
将新的Swift文件添加到名为FormatDisplay.swift
的项目中。 在import语句后添加以下内容:
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)
}
}
这些简单的功能应该是不言自明的。 在pace(distance:seconds:outputUnit :)
中,必须将MeasurementFormatter
的unitOptions
设置为.providedUnits
,以防止它显示速度的本地化测量值(例如mph
或kph
)。
Starting a Run - 开始跑步
现在几乎是开始跑步的时候了。 但首先,应用程序需要知道它在哪里。 为此,您将使用Core Location
。 重要的是,您的应用程序中只有一个CLLocationManager
实例,并且不会无意中删除它。
为此,请将另一个Swift文件添加到名为LocationManager.swift
的项目中。 用以下内容替换文件的内容:
import CoreLocation
class LocationManager {
static let shared = CLLocationManager()
private init() { }
}
在开始跟踪用户的位置之前,您需要进行一些项目级别的更改。
首先,单击Project Navigator
顶部的项目。
选择Capabilities
选项卡并将Background Modes
切换为ON
。 勾选Location updates
。
接下来,打开Info.plist
。 单击Information Property List
旁边的+。 从生成的下拉列表中,选择Privacy – Location When In Use Usage Description
并将其值设置为MoonRunner needs access to your location in order to record and track your run!
注意:此
Info.plist
密钥至关重要。 没有它,您的用户将永远无法授权您的应用访问位置服务。
在您的应用可以使用位置信息之前,它必须获得用户的许可。 在return true
之前,打开AppDelegate.swift
并将以下内容添加到application(_:didFinishLaunchingWithOptions:)
:
let locationManager = LocationManager.shared
locationManager.requestWhenInUseAuthorization()
打开NewRunViewController.swift
并导入CoreLocation
:
import CoreLocation
接下来,在run
属性之后添加以下内容:
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
是您用于启动和停止位置服务的对象。 -
seconds
跟踪跑步的持续时间,以秒为单位。 -
timer
将每秒触发并相应地更新UI。 -
distance
保持跑步的累积距离。 -
locationList
是一个数组,用于保存运行期间收集的所有CLLocation
对象。
在viewDidLoad()
之后添加以下内容:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
locationManager.stopUpdatingLocation()
}
这确保了当用户离开视图时停止位置更新,消耗电池以及计时器。
添加以下两种方法:
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()
将由您将很快设置的Timer
每秒调用一次。
updateDisplay()
使用您在FormatDisplay.swift
中构建的花哨格式化功能来使用当前运行的详细信息更新UI。
Core Location
通过其CLLocationManagerDelegate
报告位置更新。 现在将其添加到文件末尾的扩展中:
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
更新用户的位置时,都会调用此委托方法,从而提供CLLocation
对象的数组。 通常此数组仅包含一个对象,但如果有更多,则按时间排序,最后一个位置为最后一个。
CLLocation
包含一些很好的信息,包括读取的纬度,经度和时间戳。
在盲目接受读取数据之前,值得检查数据的准确性。 如果设备不确定其读数在距离用户实际位置20米的范围内,则最好将其保留在数据集之外。 确保数据是最近的也很重要。
注意:当设备首次开始缩小用户的一般区域时,此检查在跑步开始时尤为重要。 在那个阶段,它可能会更新前几个点的一些不准确的数据。
如果CLLocation
通过了检查,则它与最近保存的点之间的距离将添加到运行的累积距离。 distance(from:)
非常方便,考虑到一些涉及地球曲率的令人惊讶的困难数学,并以米为单位返回距离。
最后,位置对象本身被添加到不断增长的位置阵列中。
现在在NewRunViewController
(不是扩展中)中添加以下方法:
private func startLocationUpdates() {
locationManager.delegate = self
locationManager.activityType = .fitness
locationManager.distanceFilter = 10
locationManager.startUpdatingLocation()
}
您将此类作为Core Location
的委托,以便您可以接收和处理位置更新。
activityType
参数专门针对这样的应用程序。 它有助于设备在整个用户的运行过程中智能地节省电力,例如当他们停下来越过道路时。
最后,您将distanceFilter
设置为10米。 与activityType
相反,此参数不会影响电池寿命。 activityType
用于读取,distanceFilter
用于报告读取。
正如您稍后进行测试后所看到的那样,位置读数可能会偏离直线。 更高的distanceFilter
可以减少zigging
和zagging
,从而为您提供更准确的线条。 不幸的是,过高的过滤器会使您的读数像素化。 这就是为什么10米是一个很好的平衡。
最后,您告诉Core Location
开始获取位置更新!
要实际开始运行,请将这些行添加到startRun()
的末尾:
seconds = 0
distance = Measurement(value: 0, unit: UnitLength.meters)
locationList.removeAll()
updateDisplay()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.eachSecond()
}
startLocationUpdates()
这会将跑步期间要更新的所有值重置为初始状态,启动每秒将触发的Timer
,并开始收集位置更新。
Saving the Run - 保存跑步
在某些时候,您的用户会感到疲倦并停止跑步。 你有UI可以做到这一点,但你还需要保存跑步的数据,否则你的用户会非常不高兴看到所有这些努力都浪费掉了。
将以下方法添加到NewRunViewController.swift
中的NewRunViewController
:
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
}
如果你在Swift 3之前使用过Core Data,你会发现iOS 10 Core Data支持的强大和简单。 您可以像创建任何其他Swift对象一样创建新的Run
对象并初始化其值。 然后,为记录的每个CLLocation
创建一个Location
对象,仅保存相关数据。 最后,使用自动生成的addToLocations(_ :)
将每个新Location
添加到Run
中。
当用户结束运行时,您希望停止跟踪位置。 将以下内容添加到stopRun()
的末尾:
locationManager.stopUpdatingLocation()
最后,在stopTapped()
中找到标题为“Save”
的UIAlertAction
并添加对self.saveRun()
的调用,使其看起来像这样:
alertController.addAction(UIAlertAction(title: "Save", style: .default) { _ in
self.stopRun()
self.saveRun() // ADD THIS LINE!
self.performSegue(withIdentifier: .details, sender: nil)
})
后记
本篇主要讲述了仿Runkeeper的简单实现,感兴趣的给个赞或者关注~~~