跑步激励追踪应用 Runkeeper 目前有 4000 万用户 ! 本教程将教您开发一款类 Runkeeper 应用,您将会学到以下内容:
成果是什么? 开发一款 app — MoonRunner — 徽章系统基于太阳系中的行星和卫星!
开始本教程之前, 你应该熟悉 Storyboards 和 Core Data. 如果您绝得需要复习下知识,请查阅链接教程.
本教程同时也使用了 iOS 10 中新增加的 Measurement 和 MeasurementFormatter 功能. 更多了解请观看视频.
鉴于内容众多,本教程将分为两部分. 第一部分重点讲解 记录跑步数据和地图路线展示. 第二部分介绍了徽章系统.
下载 项目模板. 其中包括要完成本教程的所有文件和资源.
花费几分钟熟悉下项目. Main.storyboard 已经包含了 所有 UI 界面. 将
中关于 Core Data 的模板代码移到 CoreDataStack.swift 中.Assets.xcassets 中包含了将要使用的图片和声音文件.
- AppDelegate
MoonRunner 使用 Core Data 相对简单, 仅仅使用了两个实体:
和
- Run
.
- Location
打开 MoonRunner.xcdatamodeld 同时创建两个实体: Run 和 Location. 在
中添加如下属性:
- Run
有三个属性:
- Run
,
- distance
和
- duration
. 其中有一个关联,
- timestamp
, 关联到
- locations
实体.
- Location
接着, 给
添加如下属性:
- Location
也有三个属性:
- Location
,
- latitude
和
- longitude
及一个关联,
- timestamp
.
- run
选择关联实体同时验证
关联的 Inverse 属性 已经变为 "run".
- locations
选择
关联, 设置 Type 类型 为 To Many, 同时在 Data Model Inspector's Relationship 的面板 选中 Ordered .
- locations
最后, 在 Data Model Inspector 面板中分别验证
和
- Run
实体的 Codegen 属性 设置为 Class Definition (这是默认设置).
- Location
编译项目让 Xcode 生成 Core Data 模型对应的 Swift 代码.
打开 RunDetailsViewController.swift ,在
:
- viewDidLoad()之前添加如下代码
- var run: Run!
- 接着, 在
- viewDidLoad()
- 之后添加方法:
- private func configureView() {
- }
最后, 在
中
- viewDidLoad()
- super.viewDidLoad()之后添加
.
- configureView()
- configureView()
这个设置是 app 完成导航的最低要求.
打开 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
- }
停止按钮和
在 storyboard 中默认为隐藏状态 . 这些实例用于在 跑步状态和非跑步状态进行切换.
- UIStackView
在
- 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
- }
- }
- }
大家都知道,storyboard 的 segue 是 "字符串类型". segue 标识符是一个字符串 并且没有错误检查. 在 StoryboardSupport.swift 文件中,使用协议和枚举及一点点魔法, 你就能避免使用 "字符串类型" 带来的不便.
接着, 在
:
- 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
编译并运行. 按下 "New Run" 按钮接着再按 "Start" 按钮. 验证 UI 界面已经变为了 "跑步模式":
按下 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 latitudeintrigger specification
这是正常的,对于你而言这并不代表一个错误.
iOS 10 引入了新功能,使其更容易使用和显示度量单位. 跑步者度量进度往往采用速度 (单位距离消耗的时间),它是速度(单位时间的距离) 的倒数. 你必须扩展 UnitSpeed 来实现这种计算方式.
项目中添加一个文件: 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
用于数学计算.
- 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))
- }
- }
是 Foundation 中 Units 下的一个类 .
- UnitSpeed
单位为 "米 / 秒". 你的扩展中可以让速度 按照 分 / 千米 或分 / 米来表示.
- UnitSpeed的默认
你需要统一的方式来显示这些定量信息如距离, 时间, 速度和日期.
和
- 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<UnitLength>) -> 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<UnitLength>, 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设置为
避免它显示本地化的速度测量单位 (例如 mph 或 kph).
- .providedUnits
基本上可以开始跑步了. 但是首先, app 需要知道它在哪里. 为此, 你将会使用 Core Location. 重要的是,在你的 app 中只能有一个
实例,它不能被无意中删除.
- CLLocationManager
为此, 添加一个 Swift 文件,命名为 LocationManager.swift. 将其内容替换为:
- import CoreLocation
- class LocationManager {
- static let shared = CLLocationManager()
- private init() { }
- }
在开始追踪用户位置之前,你必须做几个项目级别的修改.
首先, 在项目导航栏顶部点击项目.
选择 Capabilities 栏开启 Background Modes . 选中 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!"
在你的 app 使用位置信息之前, 设备必须从用户那获得授权. 打开 AppDelegate.swift 在
- application(_:didFinishLaunchingWithOptions:)中添加如下代码,在
:
- return true 之前即可
- 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
每秒触发一次并相应的更新 UI.
- timer
存储累计跑步距离.
- distance
是一个数组,用于保存跑步期间所有的
- locationList
对象.
- CLLocation
:
- viewDidLoad()之后添加以下方法
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- timer?.invalidate()
- locationManager.stopUpdatingLocation()
- }
当用户离开跑步页面时,这确保了 timer 和带来大耗电量位置更新的停止.
添加以下两个方法:
- 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()
使用 FormatDisplay.swift 中实现的格式化功能来更新 UI.
- updateDisplay()
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
最后, 位置对象添加到不断增长的位置数组里.
(不是分类中):
- 回到NewRunViewController中添加如下方法
- private func startLocationUpdates() {
- locationManager.delegate = self
- locationManager.activityType = .fitness
- locationManager.distanceFilter = 10
- locationManager.startUpdatingLocation()
- }
你需要实现这个代理,这样你能够接收和处理位置更新.
参数应该这样设置. 这样可以帮助设备在用户跑步过程中节省电量, 比如他们在交叉路口停下来.
- 跑步类应用中activityType
最后, 设置
为 10 米.
- distanceFilter
, 这个参数不会影响电量消耗.
- 而不像activityType
在跑步测试后, 您将看到,位置读数可能会偏离直线.
值设置的过高可以减少上下波动,因此可以更加准确的展示路线. 不幸的是, 值设置的太高了会是读数像素化. 这就是为什么 10 米是一个折中值.
- distanceFilter
最后, 启动 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
某一时刻, 你的用户感觉累了并停止跑步. UI 界面已经有让用户保存数据的功能, 但是你同样需要自动保存跑步数据,否则您的用户就会因为未保存数据所有的努力白费而不高兴.
中添加如下方法:
- 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
- }
如果你使用过 Swift3 之前版本的 Core Data , 你将会发现 iOS 10 中对 Core Data 支持的强大功能和简洁性. 创建一个 new
实例并初始化. 接着为每个记录的
- Run
创建一个
- CLLocation
实例, 只保存相关的数据. 最后, 使用自动生成的方法
- Location
将每个
- addToLocations(_:)
添加到 new
- 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)
- })
应用发布前,你应该在真机上进行测试, 但每次你想测试 MoonRunner 时,不必出去跑步.
编译并运行模拟器. 在按下 "New Run" 按钮之前, 从模拟器菜单中选择 Debug\Location\City Run .
现在, 按下 New Run, 接着按下 Start ,模拟器已经开始模拟跑步.
上述工作完成后, 我们需要展示用户的目的地和完成情况.
打开 RunDetailsViewController.swift 同时将
中替换为:
- configureView()
- 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)"
- }
格式化跑步详细信息并显示.
在地图上显示跑步信息有些工作量. 需三步完成:
用于描述画线.
- MKOverlay
添加如下方法:
- 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
在文件末尾,大括号之后添加如下分类:
- 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 每次只能显示一个覆盖层. 现在, 如果 覆盖层 是一个
(线段的集合), 返回配置为黑色的 MapKit
- MKPolyine
. 接下来将会彩色化这些线段.
- 的MKPolylineRenderer
最后, 你需要创建一个 overlay.
(不是分类中):
- RunDetailsViewController中添加如下方法
- 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)
- }
这里, 你需要将跑步位置记录转换成
- MKPolyline需求的
类型
- CLLocationCoordinate2D
现在将这些整合到一起. 添加如下方法:
- 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())
- }
这里,设置地图区域并且添加覆盖层.
现在,
.
- configureView()方法结尾添加如下调用
- loadMap()
编译并运行. 当你保存完成的跑步, 你将会看到跑步足迹地图!
- 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 ,对你来说这并不代表错误.
这个应用程序已经相当不错了,但是如果你用颜色来区别速度的差异,地图可能会更好。
增加一个 Cocoa Touch 类文件, 将其命名为 MulticolorPolyline 作为 MKPolyline 的子类.
打开 MulticolorPolyline.swift 导入 MapKit:
- import MapKit
类中添加 color 属性:
- var color = UIColor.black
哇, 就是如此简单! :] 现在, 难度来了, 打开 RunDetailsViewController.swift 添加如下方法:
- 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)
- }
这里, 我们定义了三个基本颜色:红色,黄色和绿色. 接着你就可以根据从最慢到最快的速度范围生成混合颜色.
将
中的代码替换为
- polyLine()
- 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
- }
以下是新版本的内容:
对象并成对保存.
- CLLocation
,并设置颜色.
- MulticolorPolyline
行, 你将会提示编译错误. 使用下面的代码替换:
- 在loadMap()中的 mapView.add(polyLine())
- mapView.addOverlays(polyLine())
现在
分类中使用如下代码替换
- MKMapViewDelegate
:
- mapView(_:rendererFor:)
- 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
编译并运行! 让模拟器启动慢跑任务,最后看看彩色地图!
跑后的地图是惊人的, 但是如何在跑步期间也展示一个地图呢
在 storyboard 中 使用
s 即可方便添加一个!
- UIStackView
首先, 打开 NewRunViewController.swift 并导入 MapKit:
- import MapKit
接着, 打开 Main.storyboard 并找到 New Run View Controller Scene. 确保大纲视图可见. 如果不可见, 点击红框标注的按钮:
向其中拖拽一个
并将其放到 Top Stack View 和 Button Stack View 之间. 确保其实在他们的之间而不是在任何一个之中. 双击它并将其命名为 Map Container View.
- UIView
在 Attributes Inspector 中, 选中 Drawing 下的 Hidden .
在大纲视图中, Control + 拖拽 从 Map Container View 到 Top Stack View 同时在弹框中选择 Equal Widths .
拖拽一个
添加到 Map Container View. 按下 "Add New Constraints" 按钮 (又名" 钛战机按钮 ") 同时设置 4 个约束为 0. 确保"Constrain to margins" 非选中状态. 点击 Add 4 Constraints.
- MKMapView
大纲视图中选中 Map View, 打开 Size Inspector (View\Utilities\Show Size Inspector). 在 constraint 区域双击 Bottom Space to: Superview.
改变优先次序为 High (750).
在大纲视图, Control + 拖拽 从 Map View 到 New Run View Controller 同时选中 delegate.
打开 Assistant Editor, 确保是在 NewRunViewController.swift 并且 从 Map View Control + 拖拽 创建一个名为 mapView 的 outlet. 从 Map Container ViewControl + 拖拽 创建一个名为 mapContainerView 的 outlet.
关闭 Assistant Editor 并打开 NewRunViewController.swift.
在
顶部添加如下代码:
- startRun()
- mapContainerView.isHidden = false
- mapView.removeOverlays(mapView.overlays)
顶部 添加如下代码:
- 在 stopRun()
- mapContainerView.isHidden = true
现在, 你需要一个
来进行线段的渲染. 在文件的末尾添加如下分类:
- MKMapViewDelegate
- 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
- }
- }
除了线是蓝色,这个同 RunDetailsViewController.swift 中的代理很像.
最后, 你只需要添加线段图层并更新地图区域,以使地图显示区域为当前跑步区域.
方法中的 代码
- 在 locationManager(_:didUpdateLocations:)
之下添加代码:
- distance = distance + Measurement(value: delta, unit: UnitLength.meters)
- 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)
编译并运行同时启动一个跑步任务. 你将会看到实时更新的地图!
点击这里 下载 截止目前完成功能的项目代码.
你可能已经注意到用户的速度显示为 "min/mi", 因为本地配置为以米显示距离 (或者千米).
- 通过调用 FormatDisplay.pace(distance:seconds:outputUnit:)可以在
- .minutesPerMile或.minutesPerKilometer进行选择显示方式.
继续第二部分 :如何开发一款类 Runkeeper 的跑步应用之引入徽章成就系统.
一如既往, 期待您的意见和问题! :]
来源: http://blog.csdn.net/lihuinihao/article/details/73930192