版本记录
版本号 | 时间 |
---|---|
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(二)
开始
首先看下主要内容:
了解如何使用
MapKit
和CoreLocation
帮助用户完成地址并使用多个地址进行路线可视化。内容来自翻译。
接着看下写作环境:
Swift 5, iOS 13, Xcode 11
下面就是正文了
苹果公司一直在努力改善地图,以提供更好的土地细节,行人数据和道路覆盖率,从而缩小与竞争对手之间的差距。 因此,通过了解MapKit
和CoreLocation
,跳上Apple Maps
潮流。
在本教程中,您将创建一个名为RWRouter
的应用程序,以帮助您找到起点与其他两个位置之间的往返路线。 为此,您将使用CoreLocation
和MKLocalSearch
来获取地址数据,并使用MKDirections
查找地址之间的最快路径。
在本教程中,您将学习如何:
- 使用
CoreLocation
处理用户位置和授权请求。 - 使用
CLGeocoder
反向地理编码可将位置的坐标转换为人类可读的地址。 - 使用
MKLocalSearchCompleter
自动完成地址。 - 使用
MKRoute
生成路线并使用MapKit
显示它们。
现在,您准备好进入地图应用程序了!
打开启动文件夹。
应用程序的第一个视图具有三个text field
:一个用于起始/结束地址,两个用于中间的停靠点。 第二个视图具有地图视图和table view
,以显示路线和方向。
该应用程序的布局已完成,但是由您决定是否添加功能。
Using MapKit With CoreLocation
CoreLocation
和MapKit
有什么区别?
-
CoreLocation
处理您的位置数据。 它使用设备上的所有可用组件,包括Wi-Fi
,GPS
,蓝牙,磁力计,气压计和蜂窝硬件。 -
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
要求您授权? 没有。
![](https://img.haomeiwen.com/i3691932/2c8dde9fbaf61465.png)
那是因为还需要处理一件事:您需要告诉用户为什么提出请求。
Autocompleting the User’s Location
您希望您的应用程序能够对用户可能要去的地方提出明智的建议。 为此,它需要知道用户当前在哪里。 因此,您需要获得访问该用户当前位置的权限。
1. Getting Authorization to Access the User’s Location
打开Supporting Info ▸ Info.plist
,然后执行以下步骤:
- 1) 将
NSLocationWhenInUseUsageDescription
添加到Info.plist
。 - 2) 保持
Type
为String
。 - 3) 将
Value
设置为显示给用户的消息,这说明了您为何要求其位置:Used to autofill the start/end location
。
![](https://img.haomeiwen.com/i3691932/125d37934475ef14.png)
重新构建并运行; alert
应该立即弹出,如预期的那样。
![](https://img.haomeiwen.com/i3691932/bb802cda7f6477da.png)
点击Allow While Using App
或Allow Once
,使location manager
知道您的位置。
注意:
NSLocationWhenInInUseUsageDescription
和requestWhenInUseAuthorization()
允许应用在运行时访问用户的位置。 从iOS 13
开始,当请求此权限级别时,还有第三个选项:Allow Once
。 在应用程序运行时,此选项将CLAuthorizationStatus
设置为authorizedWhenInUse
。 下次启动该应用程序时,它将授权重新设置为notDetermined
。即使应用在后台运行,
NSLocationAlwaysUsageDescription
和requestAlwaysAuthorization()
仍允许该应用访问用户的位置。
现在,您已经有权使用该用户的位置,现在该使该信息起作用了!
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
中。
![](https://img.haomeiwen.com/i3691932/bd7e4c49704dd4ad.png)
接下来,您需要通过实施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
键入内容时,都会从底部滑动一个带有建议的小视图。
![](https://img.haomeiwen.com/i3691932/26075ad611f2e12f.png)
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公里。
现在,构建并运行。
![](https://img.haomeiwen.com/i3691932/1503aeb35712733e.png)
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
并运行。
![](https://img.haomeiwen.com/i3691932/f5a6bbffcf8ca162.gif)
建议位置和请求用户的位置都可以。 很好!
这个难题的最后两个部分是计算路线并显示该路线的信息。
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
以不同的方式处理location
和text
类型。MKLocalSearch
处理用户的输入,而CLGeocoder
处理该位置的坐标。您仅将当前位置用作路线的起点。这使得第一部分成为需要特殊对待的部分。 - 3) 您将其余
fields
映射为text
段。 - 4) 在显示
DirectionsViewController
之前,请确保有足够的信息。 - 5) 使用
segments
和current region
构建路线。 - 6)
helper
完成后,重新启用Calculate Route
按钮。 - 7) 如果一切按计划进行,请显示
DirectionsViewController
。如果出了什么问题,请向用户显示alert
,说明发生了什么。
这里发生了很多事情。每个部分都完成一些小任务。
Build
并运行,然后尝试一些停止。由于MKLocalSearch
可以根据您周围的环境进行操作,因此它在您的手机上也很有趣。
![](https://img.haomeiwen.com/i3691932/3f76fb441fc6e6e9.png)
就是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
并运行。 现在,您将看到两点之间的线以及目的地。 看起来不错!
![](https://img.haomeiwen.com/i3691932/0be5b213acdfde07.gif)
您几乎拥有完整的应用程序。 在下一部分中,您将完成最后的步骤,通过添加说明来使您的应用有趣而有效。
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) 更新类属性
totalDistance
和totalTravelTime
以反映整个路线的总距离和时间。 - 2) 将该信息应用于视图顶部的
informationLabel
。 - 3) 将
route
添加到数组后,重新加载table view
以反映新信息。
构建并运行。 太棒了! 现在,您可以看到详细的地图视图,其中包括每个站点之间的转弯方向。
![](https://img.haomeiwen.com/i3691932/f30da6e00f60543e.gif)
恭喜您完成了您的应用程序。 在这一点上,您应该为基于地图的应用程序的工作方式奠定良好的基础。
回顾一下您学到的知识:
- 利用当前位置为您的应用提供上下文。
-
MKLocalSearchCompleter
在搜索地址时可提供丰富的用户体验。 -
MKDirections
提供了展示从一个地方到另一个地方所需要采取的步骤的能力。
为了进一步改善应用程序,请尝试允许用户选择其他交通工具类型或添加更多目的地text field
。 另一个有趣的想法可能是当用户在table view
点击一行时在地图视图中突出显示所选步骤。
MapKit
是一个功能强大的框架。 您可以显示区域的静态快照,显示室内地图等等。 看看MapKit documentation,了解您需要使用的所有内容。
后记
本篇主要讲述了基于
MapKit
和Core Location
的Routing
,感兴趣的给个赞或者关注~~~
![](https://img.haomeiwen.com/i3691932/3897e72d8e3a6474.png)
网友评论