如何编写 Runkeeper 一样的 app(1)
运动类记步 app Runkeeper 拥有超过 4 千万的用户!本教程教你如何编写 Runkeeper 这样的 app,包括:
最终成果是:你的新 app——MoonRunner——用太阳系中的行星和月亮来作为徽章。 在继续后面的教程之前,你应当熟悉 Storyboard 和 Core Data。如果你需要复习这些内容,请点击相应链接。 本教程使用了 iOS 10 的新的 Measurement 和 MeasurementFormatter 特性。要了解它们的更多细节,请点击相应屏幕录像的链接。 转入正题,本教程分为两部分。第一部分的内容主要是记录跑步数据和颜色标注地图的渲染。第二部分内容是徽章系统。 开始下载开始项目。其中包含了所有本教程中需要用到的项目文件和图形资源。 来看一下项目结构。Main.storyboard 包含了 UI。CoreDataStack.swift 将苹果的 Core Data 模板代码从 AppDelegate 中移到了单独的类中。Assets.xcassets 包含了声音和图片。 模型:Run 和 LocationMoonRunner 用到的 Core Data 代码十分简单,只用到了两个实体:Run 和 Location。 打开 MoonRunner.xcdatamodeld ,创建两个实体: Run 和 Location。Run 属性包括: Run 类包含了 3 个属性:distance、duration 和 timestamp。它只有一个关系:locations,连接了 Location 实体。
然后,为 Location 添加如下属性: Location 类也有 3 个属性:latitude、longitude 和 timestamp,以及一个关系:run。 选中 Run 实体,查看它的 locations 关系的 Inverse 属性,现在它变成了 run。 选中 location 关系,将 Type 设置为 To Many,在数据模型检视器的 Relation 面板中,勾选 Ordered 选项。 最后,在数据模型检视器的 Entity 面板中,看一眼 Run 和 Location 实体的 Codegen 属性,是不是被设置为 Class Definition(默认值)。 编译项目,让 Xcode 为 Core Data 模型生成对应的 Swift 定义。 实现 App 基本流程打开 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
}
stop 按钮和隐藏的、用于描述跑步的 UIStackView。这两个方法会在“正在跑”和“跑步中”来回切换 UI。 在 statTapped() 方法中,调用 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
}
}
}
一提到苹果的 segue ,我们就会想到“string 类型转换”。segue identifier 是一个字符串,不需要进行错误检查。利用 Swift 协议和枚举的功能,以及 StoryboardSupport.swift 一点小花招,我们就可以避免编写大量类似于“string 类型转换”的代码。 然后,在 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)
当用户点击 stop 按钮,你应当允许他保存、放弃或继续当前的这次跑步。我们用一个 alert 提示用户并获取用户的选择。 Build & run。点击 New Run 按钮,然后点 Start 按钮。你会看到 UI 变成了“跑步模式”: 点击 Stop 按钮,然后点 Save 按钮,你会进入详情页面。
Unit 和 FormattingiOS 10 出现了一个新的能力,使得测量单位更容易被使用和显示。跑步爱好者习惯于在跑步中使用 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 中提供的众多单位的一种。 UnitSpeed 的默认单位是“米/秒”。我们的扩展能够让速度以“分钟/km”或“分钟/英里”进行表达。 在整个 MoonRunner app 中,我们需要用一种规范的形式来显示距离、时间、pace 和日期。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 设置为 .providedUnits,以免它被显示成本地化的速度单位(比如 mph 或 kph)。 开始跑步马上开始跑步了。但首先,app 必须知道当前位置。而要做到这个,你必须使用 Core Location。注意在你的 app 中只有一个 CLLocationMananger,不要在疏忽大意之下删除它。 为了实现这点,新建 Swift 文件 LocationManager.swift。编辑内容为: import CoreLocation
class LocationManager {
static let shared = CLLocationManager()
private init() { }
}
这种开始记录用户位置之前,你还需要修改几处项目设置。 首先,在项目导航器中选中位于顶层的项目。 选择 Capabilities tab 将 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!
在 app 使用 location 服务之前,它必须被用户授权。打开 AppDelegate.swift,在 application(_:didFinishLaunchingWithOptions:) 方法的 return 之前加入: 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] = []
分别说明如下:
在 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() 方法每秒都会定时器被调用,而定时器在后面创建。 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 Locatoin 刷新到用户位置时调用,它会返回一个 CLLocation 对象数组的参数。通常这个数组只会有一个对象,但多个对象时,它们会按照位置更新时间进行排序。 一个 CLLocation 包含大量信息,包括经纬度和采集时间。 在毫无条件地接受数据之前,需要检查一下数据的精度。如果设备没有拿到用户真实位置 20 米范围内的数据,则这个数据应当被抛弃。同样重要的还有一点,就是保持数据足够新。
如果 CLLocation 检查通过,它和最近保存的位置之间的距离会被累加到这次跑步的累计距离中。distance(from:) 方法非常有用,它参考了地球曲率来进行复杂计算,返回一个以米为单位的长度。 最后,location 对象被添加到 locations 数组。 然后,将这个方法添加到 NewRunViewController类(不要加在扩展中): private func startLocationUpdates() {
locationManager.delegate = self
locationManager.activityType = .fitness
locationManager.distanceFilter = 10
locationManager.startUpdatingLocation()
}
你将这个类设置为 Core Location 的 delegate,这样就可以接收到位置刷新通知了。 activityType 参数是专门针对这一类型的 app 的。它有助于在用户跑步的过程中让设备进入智能节电模式,比如在横穿马路时停止位置刷新。 最后,将 distanceFilter 设置为 10 米。和 activityType 相反,这个参数不会对电池寿命有任何影响。activityType 会用于数据的采集,而 distanceFilter 只用于数据的通知。 在后面的测试中你会发现,位置数据会从直线上跑偏。将 distanceFilter 值提高有助于减少 z 形或锯齿状数据的出现,从而形成一条更精准的线条。 但太高的 distanceFilter 会导致数据颗粒化。因此 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()
这里重置了所有在跑步中会改变的变量为初始状态,启动每秒定时器,开始收集位置刷新数据。 保存跑步练习有时候,用户会感到疲倦然后停止练习。你有专门的 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
}
如果你使用过 Swift 3 之前的 Core Data,你会发现 iOS 10 的 Core Data 变得更加简单和功能强大了。我们新建了一个新的 Run 对象,填充它的属性。然后针对我们保存的 CLLocation 对象创建每个 Location 对象,填入对应的数据。最后,将所有新 Location 对象用自动生成的 addToLocations() 方法保存到 Run 中。 当用户终止练习,你需要停止记录位置。在 stopRun() 方法的末尾添加这一句: locationManager.stopUpdatingLocation() 最后,在 stopTapped() 方法中找到 title 为 Save 的 UIAlertAction,在其中调用 self.saveRun(),变成这个样子: alertController.addAction(UIAlertAction(title: "Save",style: .default) { _ in
self.stopRun()
self.saveRun() // 添加这句!!!
self.performSegue(withIdentifier: .details,sender: nil)
})
发送到模拟器虽然你可以在发布之前一直坚持在真机上测试 app,但没有必要每次测试 MoonRunner 时都做一次跑步练习。 在模拟器中 Build & run。在点击 New Run 按钮之前,从模拟器菜单中,选择 DebugLocationCity 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)"
}
将本次联系的数据格式化并显示。 在地图上绘制出跑步练习的事情还真不少。需要 3 个步骤:
新增如下方法: 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
}
}
每当地图准备绘制某个覆盖物时,它会询问委托对象绘制这个覆盖物需要用到的东西。这里,我们需要的覆盖物是一个 MKPolyine(线段集合),所以我们会返回 MapKit 中的 MKPolylineRenderer 对象,我们用这个对象来指定绘制的颜色为黑色。稍后我们会使用更多的颜色。 最后是创建覆盖物。在 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())
}
这里,首先保证要绘制的东西还在。然后设置地图的 region 并添加覆盖物。 在 configureView() 方法中加入: loadMap() Build & run。当你保存所完成的联系时,你会看到根据这次练习所画出来的地图!
添加颜色这个 app 挺不错的,但如果能够用不同的颜色将线段根据不同的步速标记出来就更好了。 新建 Cocoa Touch Class 文件,命名为 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
}
修改后的方法做了些什么:
在 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 对象,用该对象的 color 颜色来渲染线段。 Build & run。用模拟器跑出一小段距离,观察地图上的多彩线段。 一点改进计步结束后的地图固然不错,但在跑步的过程中显示地图不是更好? 在故事板中用 UIStackView 添加一个地图是很容易的。 首先,打开 NewRunViewController.swift ,导入 MapKit: import MapKit 现在,打开 Main.storyboard,找到 New Run View Controller 场景。确保打开 Document Outline 窗口。如果没有打开,请点击下图中用红色圈住的按钮: 拖一个 UIView 到 Document Outline 的 Top Stack View 和 Button Stack View 之间。确保将它放在二者之间而不是某一个之内。双击它,重命名为 Map Container View。 在属性面板中,在 Drawing 下面勾选 Hidden。 在 Document Outline 窗口,右键,从 Map Container View 拖到 Top Stack View 并选择弹出菜单中的 Equal Widths。 拖一个 MKMapView 到 Map Container View。点击 Add New Constraints 按钮(你也可以叫它“钛战机按钮”),然后将 4 边约束都设置为 0。确保 Constrain to margins 为未选中。然后点击 Add 4 Constraints。 保持 Map View 的选中状态,打开 Size 面板(ViewUtilitiesShow Size Inspector)。双击约束 Bottom Space to: Superview。 设置 priority 为高(750)。 在 Document Outline 中,右键,从 Map View 拖到 New Run View Controller 然后选择 delegate。 打开助手编辑器,确保 NewRunViewController.swift 文件打开,然后右键,从 Map View 拖到源文件中,创建一个出口,命名为 mapView。右键,从 Map Container View 拖一个新出口名为 mapContainerView。 关闭助手编辑器,打开 NewRunViewController.swift 文件。 在 startRun 方法头部添加: mapContainerView.isHidden = false
mapView.removeOverlays(mapView.overlays)
在 stopRun() 方法头部添加: mapContainerView.isHidden = true 然后要实现 MKMapViewDelegate 以便为线段的绘制提供 renderer。在文件最后新增一个扩展: 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 中写的差不多,除了线段颜色是蓝色的以外。 最后,只需要添加覆盖物和设置地图 region 以便使地图居中显示你所跑过的区域。在 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)
Build & run,重新开始计步。你会发现新地图会实时进行刷新! 结尾点击此处下载到此进度的项目。 你可能发现用户的步速始终是 min/mi,哪怕你本地化的距离单位是米(或km)。要显示本地化的距离,可以在调用 FormatDisplay.pace(distance:second:outputUnit:) 时选择 .minutesPerMile 或 .minutesPerKilometer。 在第二部分教程中,你将继续学习如何添加一个成就徽章系统。 期待你的评论和提问!:] (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |