版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.14 星期日 |
前言
MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)
3. MapKit框架详细解析(三) —— 基本使用简单示例(二)
开始
首先看一下写作环境
Swift 4, iOS 11, Xcode 9
本篇主要就是了解如何使用MapKit叠加视图将卫星和混合地图,自定义图像,注释,线条,边界和圆圈添加到标准地图。
Apple可以很容易地使用MapKit
向您的应用添加地图,但仅此一点并不十分吸引人。 幸运的是,您可以使用custom overlay views
使地图更具吸引力。
在这个MapKit
教程中,您将创建一个应用程序来展示Six Flags Magic Mountain。 对于你那里快速骑行的刺激寻求者,这个应用程序适合你。
当您完成时,您将拥有一个交互式公园地图,显示景点位置,骑行路线和角色位置。
打开入门项目。 此启动包含导航,但它还没有任何地图。
在Xcode中打开启动项目;Build和运行;你会看到一个空白的视图。 您很快就会在此处添加地图和可选择的叠加层类型。
Adding a MapView with MapKit - 使用MapKit添加MapView
打开Main.storyboard
并选择Park Map View Controller
场景。 在Object Library
中搜索map
,然后将Map View
拖放到此场景中。 将其放置在导航栏下方,使其填充视图的其余部分。
接下来,选择Add New Constraints
按钮,使用常量0添加四个约束,然后单击Add 4 Constraints
。
1. Wiring Up the MapView - 连接MapView
要对MapView
执行任何有用的操作,您需要做两件事:(1)为其设置outlet
,以及(2)设置其代理。
通过按住Option
键并在文件层次结构中左键单击ParkMapViewController.swift
,在Assistant Editor
中打开ParkMapViewController
。
然后,从map view
按住control拖动到第一个方法的正上方,如下所示:
在出现的弹出窗口中,将outlet
命名为mapView
,然后单击Connect
。
要设置地图视图的代理,请右键单击地图视图对象以打开其上下文菜单,然后从代理outlet
拖动到Park Map View Controller
,如下所示:
您还需要使ParkMapViewController
符合MKMapViewDelegate
。
首先,将此import添加到ParkMapViewController.swift
的顶部:
import MapKit
然后,在结束类花括号之后添加此扩展:
extension ParkMapViewController: MKMapViewDelegate {
}
Build并运行以查看新地图!
2. Interacting with the MapView - 与MapView交互
您将首先将地图置于公园中心。 在应用程序的Park Information
文件夹中,您将找到名为MagicMountain.plist
的文件。 打开此文件,您将看到它包含公园中点和边界信息的坐标。
您现在将为此plist创建一个模型,以便在应用程序中轻松使用。
右键单击文件导航中的Models
组,然后选择New File ...
,选择iOS \ Source \ Swift File
模板并将其命名为Park.swift
。 用以下内容替换其内容:
import UIKit
import MapKit
class Park {
var name: String?
var boundary: [CLLocationCoordinate2D] = []
var midCoordinate = CLLocationCoordinate2D()
var overlayTopLeftCoordinate = CLLocationCoordinate2D()
var overlayTopRightCoordinate = CLLocationCoordinate2D()
var overlayBottomLeftCoordinate = CLLocationCoordinate2D()
var overlayBottomRightCoordinate = CLLocationCoordinate2D()
var overlayBoundingMapRect: MKMapRect?
}
您还需要能够将Park
的值设置为plist中定义的值。
首先,添加此便捷方法以反序列化属性列表:
class func plist(_ plist: String) -> Any? {
let filePath = Bundle.main.path(forResource: plist, ofType: "plist")!
let data = FileManager.default.contents(atPath: filePath)!
return try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)
}
接下来,在给定fieldName
和字典的情况下,添加下一个方法来解析CLLocationCoordinate2D
:
static func parseCoord(dict: [String: Any], fieldName: String) -> CLLocationCoordinate2D {
guard let coord = dict[fieldName] as? String else {
return CLLocationCoordinate2D()
}
let point = CGPointFromString(coord)
return CLLocationCoordinate2DMake(CLLocationDegrees(point.x), CLLocationDegrees(point.y))
}
MapKit的API使用CLLocationCoordinate2D
来表示地理位置。
您现在终于准备为此类创建初始化程序:
init(filename: String) {
guard let properties = Park.plist(filename) as? [String : Any],
let boundaryPoints = properties["boundary"] as? [String] else { return }
midCoordinate = Park.parseCoord(dict: properties, fieldName: "midCoord")
overlayTopLeftCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayTopLeftCoord")
overlayTopRightCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayTopRightCoord")
overlayBottomLeftCoordinate = Park.parseCoord(dict: properties, fieldName: "overlayBottomLeftCoord")
let cgPoints = boundaryPoints.map { CGPointFromString($0) }
boundary = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }
}
首先,从plist文件中提取公园的坐标并将其分配给属性。 然后设置boundary
数组,稍后您将使用它来显示公园轮廓。
您可能想知道,“为什么没有从plist设置overlayBottomRightCoordinate
?”这在plist中没有提供,因为您可以从其他三个点轻松计算它。
用这个计算属性替换当前的overlayBottomRightCoordinate
:
var overlayBottomRightCoordinate: CLLocationCoordinate2D {
get {
return CLLocationCoordinate2DMake(overlayBottomLeftCoordinate.latitude,
overlayTopRightCoordinate.longitude)
}
}
最后,您需要一种方法来基于叠加坐标创建边界框。
用这个替换overlayBoundingMapRect
的定义:
var overlayBoundingMapRect: MKMapRect {
get {
let topLeft = MKMapPointForCoordinate(overlayTopLeftCoordinate)
let topRight = MKMapPointForCoordinate(overlayTopRightCoordinate)
let bottomLeft = MKMapPointForCoordinate(overlayBottomLeftCoordinate)
return MKMapRectMake(
topLeft.x,
topLeft.y,
fabs(topLeft.x - topRight.x),
fabs(topLeft.y - bottomLeft.y))
}
}
此getter
为公园的边界生成MKMapRect
对象。 这只是一个矩形,它定义了公园的大小,以公园的中点为中心。
现在是时候让这个类使用了。 打开ParkMapViewController.swift
并向其添加以下属性:
var park = Park(filename: "MagicMountain")
然后,用这个替换viewDidLoad()
:
override func viewDidLoad() {
super.viewDidLoad()
let latDelta = park.overlayTopLeftCoordinate.latitude -
park.overlayBottomRightCoordinate.latitude
// Think of a span as a tv size, measure from one corner to another
let span = MKCoordinateSpanMake(fabs(latDelta), 0.0)
let region = MKCoordinateRegionMake(park.midCoordinate, span)
mapView.region = region
}
这将创建一个纬度增量,即从公园的左上角坐标到公园的右下角坐标的距离。 您可以使用它来生成MKCoordinateSpan
,它定义了地图区域所跨越的区域。 然后使用MKCoordinateSpan
和公园的midCoordinate
创建一个MKCoordinateRegion
,将公园定位在地图视图上。
Build并运行您的应用程序,您将看到地图现在以Six Flags Magic Mountain
为中心!
好的! 你把地图集中在以公园为中心,这很不错,但并不是非常令人兴奋。 让我们通过将地图类型切换为卫星来增添趣味!
Switching The Map Type - 切换地图类型
在ParkMapViewController.swift
中,您会注意到这个方法:
@IBAction func mapTypeChanged(_ sender: UISegmentedControl) {
// TODO
}
入门项目有很多你需要做的来充实这个方法。 您是否注意到位于地图视图上方的segmented control
似乎做了很多事情?
segmented control
实际上是调用mapTypeChanged(_ :)
,但正如你在上面看到的,这个方法什么也没做!
将以下实现添加到mapTypeChanged()
:
mapView.mapType = MKMapType.init(rawValue: UInt(sender.selectedSegmentIndex)) ?? .standard
信不信由你,在您的应用中添加标准,卫星和混合地图类型就像上面的代码一样简单! 那不容易吗?
Build并运行,并尝试分段控件来更改地图类型!
即使卫星视图仍然比标准地图视图好得多,它对您的公园访客仍然没有多大帮助。 没有任何标签 - 您的用户将如何在公园内找到任何东西?
一个显而易见的方法是将UIView
放在地图视图的顶部,但是你可以更进一步,而是利用MKOverlayRenderer
的魔力为你做很多工作!
All About Overlay Views - 所有关于叠加视图
在开始创建自己的叠加视图之前,您需要了解两个关键类:MKOverlay
和MKOverlayRenderer
。
MKOverlay
告诉MapKit
你想要绘制叠加层的位置。使用该类有三个步骤:
- 1) 创建自己的实现MKOverlay protocol协议的自定义类,该协议具有两个必需属性:
coordinate
和boundingMapRect
。这些属性定义了叠加层在地图上的位置以及叠加层的大小。 - 2) 为要显示叠加层的每个区域创建类的实例。例如,在这个应用程序中,您可以为过山车覆盖层创建一个实例,为餐厅覆盖层创建另一个实例。
- 3) 最后,将叠加层添加到地图视图中。
现在,地图视图知道它应该显示叠加的位置,但它如何知道每个区域中显示的内容?
输入MKOverlayRenderer
。您将其子类化以设置要在每个点中显示的内容。例如,在这个应用程序中,您将绘制过山车或餐厅的图像。
MKOverlayRenderer
实际上只是一种特殊的UIView
,因为它继承自UIView。但是,您不应将MKOverlayRenderer
直接添加到MKMapView
。相反,MapKit
希望这是一个MKMapView
。
还记得你之前设置的地图视图代理吗?有一个代理方法,允许您返回叠加视图:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
当MapKit意识到地图视图正在显示的区域中有一个MKOverlay
对象时,它将调用此方法。
总结一下,不要将MKOverlayRenderer
对象直接添加到地图视图中;相反,您告诉地图有关MKOverlay
对象的显示,并在代理方法请求它们时返回它们。
既然您已经了解了这个理论,那么现在是时候使用这些概念了!
Adding Your Own Information - 添加自己的信息
如前所述,卫星视图仍未提供有关公园的足够信息。 您的任务是创建一个表示整个公园的叠加层的对象。
选择Overlays
组并创建一个名为ParkMapOverlay.swift
的新Swift文件。 用以下内容替换其内容:
import UIKit
import MapKit
class ParkMapOverlay: NSObject, MKOverlay {
var coordinate: CLLocationCoordinate2D
var boundingMapRect: MKMapRect
init(park: Park) {
boundingMapRect = park.overlayBoundingMapRect
coordinate = park.midCoordinate
}
}
遵循MKOverlay
意味着您还必须继承NSObject
。 最后,初始化程序只从传递的Park
对象中获取属性,并将它们设置为相应的MKOverlay
属性。
现在,您需要创建一个从MKOverlayRenderer
类派生的视图类。
在Overlays
组中创建一个名为ParkMapOverlayView.swift
的新Swift文件。 用以下内容替换其内容:
import UIKit
import MapKit
class ParkMapOverlayView: MKOverlayRenderer {
var overlayImage: UIImage
init(overlay:MKOverlay, overlayImage:UIImage) {
self.overlayImage = overlayImage
super.init(overlay: overlay)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let imageReference = overlayImage.cgImage else { return }
let rect = self.rect(for: overlay.boundingMapRect)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0.0, y: -rect.size.height)
context.draw(imageReference, in: rect)
}
}
init(overlay:overlayImage :)
通过提供第二个参数有效地覆盖了基本方法init(overlay :)
。
draw
是这堂课的真正做东西的地方。 它定义了MapKit
在给定特定的MKMapRect
,MKZoomScale
和图形上下文的CGContext
时应如何呈现此视图,以便以适当的比例将叠加图像绘制到上下文中。
Core Graphics绘图的详细信息远远超出了本教程的范围。 但是,您可以看到上面的代码使用传递的MKMapRect
来获取CGRect
,以便确定在提供的上下文中绘制UIImage
的CGImage
的位置。
现在您已同时拥有MKOverlay
和MKOverlayRenderer
,您可以将它们添加到地图视图中。
在ParkMapViewController.swift
中,将以下方法添加到类中:
func addOverlay() {
let overlay = ParkMapOverlay(park: park)
mapView.add(overlay)
}
此方法将MKOverlay
添加到地图视图中。
如果用户应选择显示地图叠加层,则loadSelectedOptions()
应调用addOverlay()
。 使用以下代码替换loadSelectedOptions()
:
func loadSelectedOptions() {
mapView.removeAnnotations(mapView.annotations)
mapView.removeOverlays(mapView.overlays)
for option in selectedOptions {
switch (option) {
case .mapOverlay:
addOverlay()
default:
break;
}
}
}
每当用户关闭选项选择视图时,应用程序调用loadSelectedOptions()
,然后确定所选选项,并调用适当的方法在地图视图上呈现这些选择。
loadSelectedOptions()
还会删除可能存在的任何annotations
和overlays
,以便您不会最终出现重复的渲染。 这不一定有效,但它是从地图中清除先前项目的简单方法。
要实现代理方法,请将以下方法添加到文件底部的MKMapViewDelegate
扩展中:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is ParkMapOverlay {
return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))
}
return MKOverlayRenderer()
}
当应用程序确定MKOverlay
在视图中时,地图视图将上述方法作为委托调用。
在这里,您检查叠加层是否属于类型ParkMapOverlay
。 如果是这样,则加载叠加图像,使用叠加图像创建ParkMapOverlayView
实例,并将此实例返回给调用者。
但是有一个小问题 - 那可疑的小overlay_park
图片来自哪里?
这是一个PNG文件,其目的是覆盖公园边界的地图视图。 overlay_park
图像(在image assets
中找到)如下所示:
Build并运行,选择Map Overlay
选项,瞧! 在地图上方绘制了公园覆盖图:
根据需要放大,缩小和移动 - 叠加视图按照您的预期进行缩放和移动。
后记
本篇主要讲述了一个叠加视图相关的简单示例,感兴趣的给个赞或者关注~~~