美文网首页
MapKit框架详细解析(十五) —— 基于MapKit和Cor

MapKit框架详细解析(十五) —— 基于MapKit和Cor

作者: 刀客传奇 | 来源:发表于2020-06-20 17:40 被阅读0次

版本记录

版本号 时间
V1.0 2020.06.20 星期六

前言

MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)
3. MapKit框架详细解析(三) —— 基本使用简单示例(二)
4. MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例(一)
5. MapKit框架详细解析(五) —— 一个叠加视图相关的简单示例(二)
6. MapKit框架详细解析(六) —— 添加自定义图块(一)
7. MapKit框架详细解析(七) —— 添加自定义图块(二)
8. MapKit框架详细解析(八) —— 添加自定义图块(三)
9. MapKit框架详细解析(九) —— 地图特定区域放大和创建自定义地图annotations(一)
10. MapKit框架详细解析(十) —— 地图特定区域放大和创建自定义地图annotations(二)
11. MapKit框架详细解析(十一) —— 自定义MapKit Tiles(一)
12. MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)
13. MapKit框架详细解析(十三) —— MapKit Overlay Views(一)
14. MapKit框架详细解析(十四) —— MapKit Overlay Views(二)

开始

首先看下主要内容:

了解如何使用MapKitCoreLocation帮助用户完成地址并使用多个地址进行路线可视化。内容来自翻译

接着看下写作环境:

Swift 5, iOS 13, Xcode 11

下面就是正文了

苹果公司一直在努力改善地图,以提供更好的土地细节,行人数据和道路覆盖率,从而缩小与竞争对手之间的差距。 因此,通过了解MapKitCoreLocation,跳上Apple Maps潮流。

在本教程中,您将创建一个名为RWRouter的应用程序,以帮助您找到起点与其他两个位置之间的往返路线。 为此,您将使用CoreLocationMKLocalSearch来获取地址数据,并使用MKDirections查找地址之间的最快路径。

在本教程中,您将学习如何:

  • 使用CoreLocation处理用户位置和授权请求。
  • 使用CLGeocoder反向地理编码可将位置的坐标转换为人类可读的地址。
  • 使用MKLocalSearchCompleter自动完成地址。
  • 使用MKRoute生成路线并使用MapKit显示它们。

现在,您准备好进入地图应用程序了!

打开启动文件夹。

应用程序的第一个视图具有三个text field:一个用于起始/结束地址,两个用于中间的停靠点。 第二个视图具有地图视图和table view,以显示路线和方向。

该应用程序的布局已完成,但是由您决定是否添加功能。


Using MapKit With CoreLocation

CoreLocationMapKit有什么区别?

  • CoreLocation处理您的位置数据。 它使用设备上的所有可用组件,包括Wi-FiGPS,蓝牙,磁力计,气压计和蜂窝硬件。
  • MapKit全部涉及视觉操作(如渲染地图)和用户友好的操作(如地址搜索和路线指示)。 毕竟,谁想输入纬度和经度而不是人类可读的地址?

在接下来的步骤中,您将使用CoreLocation收集用户的起始位置,使用MapKit处理用户手动输入的地址,在它们之间创建往返路线,最后,使用所有数据来显示带有整个路线。Cool


Getting the User’s Location With CoreLocation

ViewControllers / RouteSelectionViewController.swift中,将以下属性添加到类的顶部:

private let locationManager = CLLocationManager()

您将locationManager声明为该类的属性,以便可以根据需要对其进行访问。

接下来,将以下代码添加到attemptLocationAccess()来设置和实例化CLLocationManager

// 1
guard CLLocationManager.locationServicesEnabled() else {
  return
}
// 2
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
// 3
locationManager.delegate = self
// 4
if CLLocationManager.authorizationStatus() == .notDetermined {
  locationManager.requestWhenInUseAuthorization()
} else {
  locationManager.requestLocation()
}

依次浏览每个编号部分:

  • 1) 使用location manager之前,最好先确保用户已启用位置服务。
  • 2) 对位置的坐标进行地理编码时,您可能会失去一些精度。 100米的精度已绰绰有余。
  • 3) location manager的代理会在新位置到达以及隐私设置更改时通知应用程序。
  • 4) 如果用户启用并授权了位置服务,则您请求当前位置。

Build并运行。 您是否收到alert要求您授权? 没有。

那是因为还需要处理一件事:您需要告诉用户为什么提出请求。


Autocompleting the User’s Location

您希望您的应用程序能够对用户可能要去的地方提出明智的建议。 为此,它需要知道用户当前在哪里。 因此,您需要获得访问该用户当前位置的权限。

1. Getting Authorization to Access the User’s Location

打开Supporting Info ▸ Info.plist,然后执行以下步骤:

  • 1) 将NSLocationWhenInUseUsageDescription添加到Info.plist
  • 2) 保持TypeString
  • 3) 将Value设置为显示给用户的消息,这说明了您为何要求其位置:Used to autofill the start/end location

重新构建并运行; alert应该立即弹出,如预期的那样。

点击Allow While Using AppAllow Once,使location manager知道您的位置。

注意:NSLocationWhenInInUseUsageDescriptionrequestWhenInUseAuthorization()允许应用在运行时访问用户的位置。 从iOS 13开始,当请求此权限级别时,还有第三个选项:Allow Once。 在应用程序运行时,此选项将CLAuthorizationStatus设置为authorizedWhenInUse。 下次启动该应用程序时,它将授权重新设置为notDetermined

即使应用在后台运行,NSLocationAlwaysUsageDescriptionrequestAlwaysAuthorization()仍允许该应用访问用户的位置。

现在,您已经有权使用该用户的位置,现在该使该信息起作用了!

2. Turning the User’s Coordinates Into an Address

您的应用收到的位置信息将以坐标的形式出现 - 但是大多数用户都希望输入其路线作为地址。

接下来,您将创建一个CLGeocoder,以对用户的位置进行反向地理编码。 反向地理编码是将位置的坐标转换为人类可读的地址的过程。

ViewControllers / RouteSelectionViewController.swift的顶部添加一个新属性:

private var currentPlace: CLPlacemark?

在这里,您可以使用currentPlace存储来自当前位置的地理编码信息。 您稍后将使用此信息来生成路线。

滚动到CLLocationManagerDelegate的末尾,并将占位符方法替换为:

func locationManager(
  _ manager: CLLocationManager, 
  didChangeAuthorization status: CLAuthorizationStatus
) {
  // 1
  guard status == .authorizedWhenInUse else {
    return
  }
  manager.requestLocation()
}

func locationManager(
  _ manager: CLLocationManager, 
  didUpdateLocations locations: [CLLocation]
) {
  guard let firstLocation = locations.first else {
    return
  }

  // TODO: Configure MKLocalSearchCompleter here...

  // 2
  CLGeocoder().reverseGeocodeLocation(firstLocation) { places, _ in
    // 3
    guard
      let firstPlace = places?.first, 
      self.originTextField.contents == nil 
      else {
        return
    }
    

    // 4
    self.currentPlace = firstPlace
    self.originTextField.text = firstPlace.abbreviation
  }
}

此代码的作用如下:

  • 1) 确保用户已授予应用访问权限信息。
  • 2) reverseGeocodeLocation(_:completionHandler :)在其完成处理程序中返回一个地标数组。 对于大多数地理编码结果,此数组将仅包含一个元素。 在极少数情况下,单个位置可能会返回许多附近的位置。 在这种情况下,places?.first就足够了。
  • 3) 由于用户可以编辑来源text field,因此最好在更改之前确保其为空。
  • 4) 存储当前位置并更新field

注意:firstPlace.abbreviation是一个扩展属性,它确定给定CLPlacemark的适当描述。

默认情况下,模拟器未设置默认位置。 要配置位置,请打开模拟器(如果尚未运行)。 在Features ▸ Location下,选择Apple

Build并运行。 地理编码完成后,您会看到Apple Campus出现在Start / End field中。

接下来,您需要通过实施MKLocalSearchCompleter处理用户输入,以实时建议位置。


Processing User Input With MKLocalSearchCompleter

仍在ViewControllers / RouteSelectionViewController.swift中,在类顶部添加另一个属性:

private let completer = MKLocalSearchCompleter()

在这里,当用户开始在text field中输入内容时,可以使用MKLocalSearchCompleter猜测最终地址。

Actions中将以下内容添加到textFieldDidChange(_ :)

// 1
if field == originTextField && currentPlace != nil {
  currentPlace = nil
  field.text = ""
}
// 2
guard let query = field.contents else {
  hideSuggestionView(animated: true)
  // 3
  if completer.isSearching {
    completer.cancel()
  }
  return
}
// 4
completer.queryFragment = query

这是您所做的:

  • 1) 如果用户编辑了origin field,则删除当前位置。
  • 2) 您确保查询中包含信息,因为将空查询发送给completer没有意义。
  • 3) 如果该field为空,并且完成程序(completer)当前正在尝试查找匹配项,则将其取消。
  • 4) 最后,您将用户的输入传递给完成者的queryFragment

MKLocalSearchCompleter使用代理模式来显示其结果。 有一种方法可以检索结果,一种可以处理可能发生的错误。

在文件的底部,为MKLocalSearchCompleterDelegate添加新的扩展名:

// MARK: - MKLocalSearchCompleterDelegate

extension RouteSelectionViewController: MKLocalSearchCompleterDelegate {
  func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    guard let firstResult = completer.results.first else {
      return
    }
    
    showSuggestion(firstResult.title)
  }

  func completer(
    _ completer: MKLocalSearchCompleter, 
    didFailWithError error: Error
  ) {
    print("Error suggesting a location: \(error.localizedDescription)")
  }
}

MKLocalSearchCompleter将许多结果返回到completerDidUpdateResults(_ :),但对于此应用程序,您将仅使用第一个。 showSuggestion(_ :)是一种帮助程序方法,用于调整自动布局约束并设置标签的text属性。

在完成程序起作用之前,最后要做的就是连接委托。 在viewDidLoad()内部添加:

completer.delegate = self

Build并运行。 现在,当您在任何text field键入内容时,都会从底部滑动一个带有建议的小视图。

1. Improving MKLocalSearchCompleter’s Accuracy

您可能会注意到,根据您所在的位置,来自completer的结果没有意义。 这是因为您从未告诉completer您当前的位置。

在将completer连接到当前位置之前,将最后几个属性添加到类的顶部:

private var editingTextField: UITextField?
private var currentRegion: MKCoordinateRegion?

您将使用text field属性来管理当用户点击建议时当前哪个field处于活动状态。 您存储对当前区域的引用,MKLocalSearch稍后将使用该引用来确定用户当前所在的位置。

在文件底部,将// TODO: Configure MKLocalSearchCompleter here…替换为:

//1
let commonDelta: CLLocationDegrees = 25 / 111
let span = MKCoordinateSpan(
  latitudeDelta: commonDelta, 
  longitudeDelta: commonDelta)
//2
let region = MKCoordinateRegion(center: firstLocation.coordinate, span: span)
currentRegion = region
completer.region = region

此代码的作用如下:

  • 1) commonDelta是指所需的缩放级别。 增加值以扩大地图覆盖范围。
  • 2) 这是您使用通过CoreLocation的委托获取的坐标创建的区域。

注意:1个纬度大约等于111公里。

现在,构建并运行。

2. Finishing the Address’ Autocomplete

尽管您可以根据输入获得本地化的结果,但是选择该建议不是很好吗? 当然可以,而且已经定义了一种方法,您可以这样做!

compressionionTapped(_ :)中添加以下内容:

hideSuggestionView(animated: true)

editingTextField?.text = suggestionLabel.text
editingTextField = nil

当用户点击建议标签时,视图会折叠,并且其文本将设置为活动text field。 但是,在此之前,您需要设置editingTextField

为此,将以下几行添加到textFieldDidBeginEditing(_ :)

hideSuggestionView(animated: true)

if completer.isSearching {
  completer.cancel()
}

editingTextField = textField

此代码可确保当用户激活text field时,先前的建议不再适用。 这也适用于completer,如果当前正在搜索,则将其重置。 最后,设置您先前定义的text field属性。

Build并运行。

建议位置和请求用户的位置都可以。 很好!

这个难题的最后两个部分是计算路线并显示该路线的信息。


Calculating Routes With MapKit

ViewControllers / RouteSelectionViewController.swift中,还有一件事情要做:您需要一种方法来管理用户的输入并将其传递给ViewControllers / DirectionsViewController.swift

幸运的是,Models / Route.swift中的结构已经具有该功能。 它具有两个属性:一个用于原点,另一个用于沿途停靠点。

您可以直接使用Helpers / RouteBuilder.swift中的辅助功能,而不必直接创建Route。 这项繁琐的工作为您转换了用户的输入。 为简洁起见,本教程不会深入探讨此文件的内部工作原理。 如果您对它的工作方式感兴趣,请在下面的评论中进行讨论!

返回ViewControllers / RouteSelectionViewController.swift,将以下内容添加到calculateButtonTapped()中:

// 1
view.endEditing(true)

calculateButton.isEnabled = false
activityIndicatorView.startAnimating()

// 2
let segment: RouteBuilder.Segment?
if let currentLocation = currentPlace?.location {
  segment = .location(currentLocation)
} else if let originValue = originTextField.contents {
  segment = .text(originValue)
} else {
  segment = nil
}

// 3
let stopSegments: [RouteBuilder.Segment] = [
  stopTextField.contents,
  extraStopTextField.contents
]
.compactMap { contents in
  if let value = contents {
    return .text(value)
  } else {
    return nil
  }
}

// 4
guard 
  let originSegment = segment, 
  !stopSegments.isEmpty 
  else {
    presentAlert(message: "Please select an origin and at least 1 stop.")
    activityIndicatorView.stopAnimating()
    calculateButton.isEnabled = true
    return
}

// 5
RouteBuilder.buildRoute(
  origin: originSegment,
  stops: stopSegments,
  within: currentRegion
) { result in
  // 6
  self.calculateButton.isEnabled = true
  self.activityIndicatorView.stopAnimating()

  // 7
  switch result {
  case .success(let route):
    let viewController = DirectionsViewController(route: route)
    self.present(viewController, animated: true)
    
  case .failure(let error):
    let errorMessage: String
  
    switch error {
    case .invalidSegment(let reason):
      errorMessage = "There was an error with: \(reason)."
    }
    
    self.presentAlert(message: errorMessage)
  }
}

这是逐步发生的事情:

  • 1) dismiss键盘并禁用Calculate Route按钮。
  • 2) 这是方便使用当前位置信息的地方。 RouteBuilder以不同的方式处理locationtext类型。MKLocalSearch处理用户的输入,而CLGeocoder处理该位置的坐标。您仅将当前位置用作路线的起点。这使得第一部分成为需要特殊对待的部分。
  • 3) 您将其余fields映射为text段。
  • 4) 在显示DirectionsViewController之前,请确保有足够的信息。
  • 5) 使用segmentscurrent region构建路线。
  • 6) helper完成后,重新启用Calculate Route按钮。
  • 7) 如果一切按计划进行,请显示DirectionsViewController。如果出了什么问题,请向用户显示alert,说明发生了什么。

这里发生了很多事情。每个部分都完成一些小任务。

Build并运行,然后尝试一些停止。由于MKLocalSearch可以根据您周围的环境进行操作,因此它在您的手机上也很有趣。

就是ViewControllers / RouteSelectionViewController.swift

现在,切换到ViewControllers / DirectionsViewController.swift完成路由。


Requesting MKRoute Directions

您已经可以看到包含您选择的位置的地图,但是缺少路线和方向。 您仍然需要执行以下操作:

  • Route线段分组以将它们链接在一起。
  • 对于每个组,创建一个MKDirections.Request以获取要显示的MKRoute
  • 完成每个请求后,刷新地图和table view以反映新数据。

要开始使用此列表,请将此属性添加到类的顶部:

private var groupedRoutes: [(startItem: MKMapItem, endItem: MKMapItem)] = []

该数组保存您请求并在视图中显示的routes。 要使用内容填充此数组,请将以下内容添加到groupAndRequestDirections()中:

guard let firstStop = route.stops.first else {
  return
}

groupedRoutes.append((route.origin, firstStop))

if route.stops.count == 2 {
  let secondStop = route.stops[1]

  groupedRoutes.append((firstStop, secondStop))
  groupedRoutes.append((secondStop, route.origin))
}

fetchNextRoute()

此方法特定于此应用。 它创建一个包含开始和结束MKMapItem的元组数组。 仔细检查数据是否有效并且有多个停靠点后,您要添加起点和第一个停靠点。 然后,如果有一个额外的停靠站,您可以再添加两个组。 最后一组是回程,在起点处结束。

现在,您已将routes分为不同的起点和终点,可以随时将它们输入到路线请求中。

由于可能要请求的路线很多,因此您将使用递归来遍历这些路线。 如果您不熟悉这个概念,那就像是while循环。 主要区别在于,递归方法在完成任务时将自行调用。

注意:如果您想了解更多信息,请阅读 weheartswift.com’s recursion article

要查看实际效果,请将以下内容添加到fetchNextRoute()中:

// 1
guard !groupedRoutes.isEmpty else {
  activityIndicatorView.stopAnimating()
  return
}

// 2
let nextGroup = groupedRoutes.removeFirst()
let request = MKDirections.Request()

// 3
request.source = nextGroup.startItem
request.destination = nextGroup.endItem

let directions = MKDirections(request: request)

// 4
directions.calculate { response, error in
  guard let mapRoute = response?.routes.first else {
    self.informationLabel.text = error?.localizedDescription
    self.activityIndicatorView.stopAnimating()
    return
  }

  // 5
  self.updateView(with: mapRoute)
  self.fetchNextRoute()
}

此代码的作用如下:

  • 1) 这是打破递归循环的条件。 没有这个,应用程序将遭受无限循环的命运。
  • 2) 要努力达到突破状态,您需要对groupedRoutes进行突变。 在这里,您要请求的组是数组中的第一个。
  • 3) 您将请求配置为使用选定的元组值作为源和目标。
  • 4) 配置请求后,可以使用MKDirections实例来计算路线。
  • 5) 如果一切顺利,请使用新的路线信息更新视图并请求路线的下一段。

fetchNextRoute()将在calculate(completionHandler :)完成后继续调用自身。 这允许应用在用户请求路线的每个部分后显示新信息。


Rendering Routes Into the Map

要开始显示此新信息,请将以下内容添加到updateView(with :)

let padding: CGFloat = 8
mapView.addOverlay(mapRoute.polyline)
mapView.setVisibleMapRect(
  mapView.visibleMapRect.union(
    mapRoute.polyline.boundingMapRect
  ),
  edgePadding: UIEdgeInsets(
    top: 0,
    left: padding,
    bottom: padding,
    right: padding
  ),
  animated: true
)

// TODO: Update the header and table view...

MKRoute提供了一些有趣的信息。 polyline包含沿路线准备在地图视图中显示的点。 您可以将其直接添加到地图视图中,因为折线(polyline)是从MKOverlay继承的。

接下来,您更新地图视图的可见区域,以确保已查看添加到地图的新信息。

这还不足以使路线显示在地图视图上。 您需要MKMapViewDelegate来配置地图视图如何绘制显示路线的线。

将此添加到文件底部:

// MARK: - MKMapViewDelegate

extension DirectionsViewController: MKMapViewDelegate {
  func mapView(
    _ mapView: MKMapView, 
    rendererFor overlay: MKOverlay
  ) -> MKOverlayRenderer {
    let renderer = MKPolylineRenderer(overlay: overlay)

    renderer.strokeColor = .systemBlue
    renderer.lineWidth = 3
    
    return renderer
  }
}

此代理方法使您可以精确定义渲染线的外观。 对于此应用程序,您使用熟悉的蓝色来表示路线。

最后,要让地图视图使用此委托实现,请将此行添加到viewDidLoad()中:

mapView.delegate = self

Build并运行。 现在,您将看到两点之间的线以及目的地。 看起来不错!

您几乎拥有完整的应用程序。 在下一部分中,您将完成最后的步骤,通过添加说明来使您的应用有趣而有效。


Walking Through Each MKRoute.Step

至此,您已经实现了大多数table view的数据源。 但是,只对tableView(_:cellForRowAt :)进行了处理。 接下来要完成

tableView(_:cellForRowAt :)的当前内容替换为:

let cell = { () -> UITableViewCell in
  guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) 
  else {
    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
    cell.selectionStyle = .none
    return cell
  }
  return cell
}()

let route = mapRoutes[indexPath.section]
let step = route.steps[indexPath.row + 1]

cell.textLabel?.text = "\(indexPath.row + 1): \(step.notice ?? step.instructions)"
cell.detailTextLabel?.text = distanceFormatter.string(
  fromDistance: step.distance
)

return cell

这里有两件事。 首先,您使用MKRoute.Step设置单元格的textLabel。 每个步骤都有关于去往何处的说明,并偶尔会发出警告以警告用户有关途中的危险。

此外,在阅读路线时,步距也很有用。 这样就可以告诉汽车驾驶员:“Turn right on Main Street in two miles”。 您可以使用MKDistanceFormatter格式化距离,该距离在此类的顶部声明。

添加最后的代码,在updateView(with :)中替换// TODO: Update the header and table view…为:

// 1
totalDistance += mapRoute.distance
totalTravelTime += mapRoute.expectedTravelTime

// 2
let informationComponents = [
  totalTravelTime.formatted,
  "• \(distanceFormatter.string(fromDistance: totalDistance))"
]
informationLabel.text = informationComponents.joined(separator: " ")

// 3
mapRoutes.append(mapRoute)
tableView.reloadData()

使用此代码,您:

  • 1) 更新类属性totalDistancetotalTravelTime以反映整个路线的总距离和时间。
  • 2) 将该信息应用于视图顶部的informationLabel
  • 3) 将route添加到数组后,重新加载table view以反映新信息。

构建并运行。 太棒了! 现在,您可以看到详细的地图视图,其中包括每个站点之间的转弯方向。

恭喜您完成了您的应用程序。 在这一点上,您应该为基于地图的应用程序的工作方式奠定良好的基础。

回顾一下您学到的知识:

  • 利用当前位置为您的应用提供上下文。
  • MKLocalSearchCompleter在搜索地址时可提供丰富的用户体验。
  • MKDirections提供了展示从一个地方到另一个地方所需要采取的步骤的能力。

为了进一步改善应用程序,请尝试允许用户选择其他交通工具类型或添加更多目的地text field。 另一个有趣的想法可能是当用户在table view点击一行时在地图视图中突出显示所选步骤。

MapKit是一个功能强大的框架。 您可以显示区域的静态快照,显示室内地图等等。 看看MapKit documentation,了解您需要使用的所有内容。

后记

本篇主要讲述了基于MapKitCore LocationRouting,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:MapKit框架详细解析(十五) —— 基于MapKit和Cor

      本文链接:https://www.haomeiwen.com/subject/lvfgxktx.html