版本记录
版本号 | 时间 |
---|---|
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(二)
开始
首先看下主要内容:
在本
MapKit Overlay
教程中,您将学习如何在原生iOS
地图上绘制图像和线条,以使其对用户更具交互性。本文内容来自翻译。
下面看一下写作环境:
Swift 5, iOS 13, Xcode 11
下面就是正文了。
虽然MapKit
可以轻松地将地图添加到您的应用程序中,但是仅靠这一点并不是很吸引人。 幸运的是,您可以使用自定义叠加视图(custom overlay views)
来制作更具吸引力的地图。
在此MapKit
教程中,您将创建一个展示Six Flags Magic Mountain的应用。 完成后,您将获得一个交互式的公园地图,其中显示了景点,乘车路线和角色位置。 这个程序适合所有您在那里快速寻求刺激的人。
在Xcode中打开入门项目。
入门项目包括您将要使用的地图以及用于打开和关闭不同类型叠加层(overlays)
的按钮。
Build
并运行。 您会看到以下内容:
All About Overlay Views
在开始创建叠加视图(overlay views)
之前,您需要了解两个关键类:MKOverlay
和MKOverlayRenderer
。
MKOverlay
告诉MapKit
您希望它在何处绘制叠加层。使用此类的三个步骤:
- 1) 首先,创建实现
MKOverlay
协议protocol的自定义类,该类具有两个必需的属性:coordinate
和boundingMapRect
。这些属性定义了叠加层在地图上的位置及其大小。 - 2) 然后,为要显示叠加层的每个区域创建类的实例。例如,在此应用中,您将为过山车叠加层创建一个实例,为餐厅叠加层创建一个实例。
- 3) 最后,将叠加层添加到地图视图中。
此时,地图知道应该在哪里显示叠加层。但是它不知道在每个区域显示什么。
这就是MKOverlayRenderer
的作用。对其进行子类化可以设置要在每个位置显示的内容。
例如,在此应用中,您将绘制过山车或餐厅的图像。 MapKit
期望提供一个MKMapView
对象,并且此类定义地图视图使用的绘图基础结构。
看一下入门项目。在ContentView.swift
中,您将看到一个代理方法,该方法可让您返回叠加视图(overlay view)
:
func mapView(
_ mapView: MKMapView,
rendererFor overlay: MKOverlay
) -> MKOverlayRenderer
当MapKit
意识到在地图视图显示的区域中存在MKOverlay
对象时,MapKit
会调用此方法。
综上所述,您无需将MKOverlayRenderer
对象直接添加到地图视图中。 取而代之的是,您告诉地图有关要显示的MKOverlay
对象的信息,并在委托方法请求它们时返回MKOverlayRenderers
。
现在,您已经了解了理论,是时候使用这些概念了!
Adding Your Information to the Map
目前,该地图无法提供有关公园的足够信息。 您的任务是创建一个代表整个公园的叠加层的对象。
首先,选择Overlays
组,然后创建一个名为ParkMapOverlay.swift
的新Swift
文件。 然后将其内容替换为:
import MapKit
class ParkMapOverlay: NSObject, MKOverlay {
let coordinate: CLLocationCoordinate2D
let boundingMapRect: MKMapRect
init(park: Park) {
boundingMapRect = park.overlayBoundingMapRect
coordinate = park.midCoordinate
}
}
符合MKOverlay
会强制您从NSObject
继承。 初始化程序从传递的Park
对象(已在入门项目中)获取属性,并将其设置为相应的MKOverlay
属性。
接下来,您需要创建一个MKOverlayRenderer
,它知道如何绘制此叠加层。
在Overlays
组中创建一个名为ParkMapOverlayView.swift
的新Swift文件。 将其内容替换为:
import MapKit
class ParkMapOverlayView: MKOverlayRenderer {
let overlayImage: UIImage
// 1
init(overlay: MKOverlay, overlayImage: UIImage) {
self.overlayImage = overlayImage
super.init(overlay: overlay)
}
// 2
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)
}
}
以下是您添加的内容的细分:
- 1)
init(overlay:overlayImage :)
通过提供第二个参数来覆盖基本方法init(overlay :)
。 - 2)
draw(_:zoomScale:in :)
是此类的真实内容。 它定义了MapKit
在给定特定MKMapRect
,MKZoomScale
和图形上下文的CGContext
时应如何呈现此视图,目的是以适当的比例将覆盖图像(overlay image)
绘制到上下文上。
注意:
Core Graphics
绘图的详细信息不在本教程的讨论范围之内。 但是,您可以看到上面的代码使用传递的MKMapRect
来获取一个CGRect
,以便在其中提供在所提供的上下文中绘制图像。
很好,现在您已经有了MKOverlay
和MKOverlayRenderer
,将它们添加到地图视图中。
Creating Your First Map Overlay
在ContentView.swift
中,找到addOverlay()
并将其TODO
内容更改为:
let overlay = ParkMapOverlay(park: park)
mapView.addOverlay(overlay)
此方法将ParkMapOverlay
添加到地图视图(map view)
。
看一下updateMapOverlayViews()
。 您会看到,当用户点击导航栏中的按钮以显示地图叠加层时,就会调用addOverlay()
。 现在,您已经添加了必要的代码,随即显示叠加层。
请注意,updateMapOverlayViews()
还删除了可能存在的所有注释和叠加层(annotations and overlays)
,因此您不会得到重复的渲染。 这不一定有效,但这是一种从地图上清除先前项目的简单方法。
站在您与您在地图上看到新实现的叠加层之间的最后一步是前面提到的mapView(_:rendererFor :)
。 将其当前的TODO
实施替换为:
if overlay is ParkMapOverlay {
return ParkMapOverlayView(
overlay: overlay,
overlayImage: UIImage(imageLiteralResourceName: "overlay_park"))
}
当MapKit
确定MKOverlay
在视图中时,它将调用此委托方法以获得渲染器。
在这里,您可以检查叠加层是否为ParkMapOverlay
类类型。 如果是这样,则加载叠加图像,使用叠加图像创建一个ParkMapOverlayView实例,然后将此实例返回给调用者。
不过,这里缺少一小块:可疑的overlay_park
小图像是从哪里来的? 这是将地图与定义的公园边界叠加在一起的PNG
。 在Assets.xcassets
中找到的overlay_park
图像如下所示:
构建并运行,启用屏幕顶部的:Overlay:
选项,然后加油! 这是在地图顶部绘制的公园叠加层:
放大,缩小并四处移动。 覆盖层会按预期缩放和移动。Cool!
Adding Annotations
如果您曾经在原生Maps
应用中搜索过某个位置,那么您会看到这些彩色图钉(pins)
出现在地图上。 这些是使用MKAnnotationView
创建的注释(annotations)
。 您可以在自己的应用程序中使用注释,并使用所需的任何图像,而不仅仅是
pins`!
Annotations
有助于突出显示公园游客的特定兴趣点。 它们的工作方式类似于MKOverlay
和MKOverlayRenderer
,但是您将使用MKAnnotation
和MKAnnotationView
。
1. Writing Your First Annotation
首先,在Annotations
组中创建一个名为AttractionAnnotation.swift
的新Swift
文件。 然后,将其内容替换为:
import MapKit
// 1
enum AttractionType: Int {
case misc = 0
case ride
case food
case firstAid
func image() -> UIImage {
switch self {
case .misc:
return UIImage(imageLiteralResourceName: "star")
case .ride:
return UIImage(imageLiteralResourceName: "ride")
case .food:
return UIImage(imageLiteralResourceName: "food")
case .firstAid:
return UIImage(imageLiteralResourceName: "firstaid")
}
}
}
// 2
class AttractionAnnotation: NSObject, MKAnnotation {
// 3
let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
let type: AttractionType
// 4
init(
coordinate: CLLocationCoordinate2D,
title: String,
subtitle: String,
type: AttractionType
) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.type = type
}
}
这是您添加的内容:
- 1)
AttractionType
可帮助您将每个景点分类为一种类型。 该枚举列出了四种类型的注释:杂项,乘车,食物和急救(misc, rides, foods and first aid)
。 还有一种方便的方法来获取正确的annotation
图像。 - 2) 您创建此类并使其符合 MKAnnotation。
- 3) 与
MKOverlay
非常相似,MKAnnotation
具有必需的coordinate
属性。 您定义了一些特定于此实现的属性。 - 4) 最后,定义一个初始化程序,该初始化程序可让您为每个属性分配值。
接下来,您将创建一个MKAnnotationView
的特定实例以用于您的annotations
。
2. Associating a View With Your Annotation
首先,在Annotations
组中创建另一个名为AttractionAnnotationView.swift
的Swift
文件。 然后,将其内容替换为以下代码段:
import MapKit
class AttractionAnnotationView: MKAnnotationView {
// 1
// Required for MKAnnotationView
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// 2
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
guard
let attractionAnnotation = self.annotation as? AttractionAnnotation else {
return
}
image = attractionAnnotation.type.image()
}
}
以下是代码细分:
- 1)
MKAnnotationView
需要使用init(coder :)
。 如果没有定义,错误将阻止您构建和运行应用程序。 为了避免这种情况,请定义它并调用其超类初始化程序。 - 2) 您还可以覆盖
init(annotation:reuseIdentifier :)
并根据annotation
的type
属性设置其他注释(annotation
)图像。
创建annotation
及其相关视图之后,就可以开始向地图视图添加annotation
了!
Adding Annotations to the Map
要确定每个annotation
的位置,请使用MagicMountainAttractions.plist
文件中的信息,该文件位于Park Information
组下。plist
文件包含有关公园景点的坐标信息和其他详细信息。
返回ContentView.swift
并将TODO:
中addAttractionPins()
的实现替换为:
// 1
guard let attractions = Park.plist("MagicMountainAttractions")
as? [[String: String]] else { return }
// 2
for attraction in attractions {
let coordinate = Park.parseCoord(dict: attraction, fieldName: "location")
let title = attraction["name"] ?? ""
let typeRawValue = Int(attraction["type"] ?? "0") ?? 0
let type = AttractionType(rawValue: typeRawValue) ?? .misc
let subtitle = attraction["subtitle"] ?? ""
// 3
let annotation = AttractionAnnotation(
coordinate: coordinate,
title: title,
subtitle: subtitle,
type: type)
mapView.addAnnotation(annotation)
}
以下是分步细分:
- 1) 首先,您阅读
MagicMountainAttractions.plist
并将其存储为字典数组。 - 2) 然后,您遍历数组中的每个字典。
- 3) 对于每个条目,您都将创建一个
AttractionAnnotation
实例以及该点的信息,并将其添加到地图视图中。
最后但并非最不重要的一点是,您需要实现另一个委托方法,该方法将MKAnnotationView
实例提供给地图视图,以便它可以自行渲染它们。
将以下方法添加到文件顶部的Coordinator
类中:
func mapView(
_ mapView: MKMapView,
viewFor annotation: MKAnnotation
) -> MKAnnotationView? {
let annotationView = AttractionAnnotationView(
annotation: annotation,
reuseIdentifier: "Attraction")
annotationView.canShowCallout = true
return annotationView
}
此方法接收选定的MKAnnotation
并使用它来创建AttractionAnnotationView
。 由于canShowCallout
属性设置为true
,因此当用户触摸注释(annotation)
时会出现一个标注(call-out)
。 最后,该方法返回注释视图(annotation view)
。
Build
并运行以查看实际中的annotations
! 不要忘记打开:Pins:
选项。
在这一点上,Attraction pins
看起来相当锋利!
到目前为止,您已经介绍了MapKit
的一些复杂部分,包括叠加层和注释(overlays and annotations)
。 但是,如果您需要使用一些绘图图元(如直线和圆)怎么办?
MapKit
框架还允许您直接绘制到地图视图上。 MapKit
为此提供了MKPolyline
,MKPolygon
和MKCircle
。 是时候尝试一下了!
I Walk The Line: MKPolyline
如果您去过魔术山(Magic Mountain)
,您就会知道歌利亚(Goliath)
过山车是一次不可思议的旅程。 一些车手喜欢在走入大门时就做出一条直线!
为了帮助这些骑手,您将画一条从公园入口到巨人的路径。
MKPolyline
是绘制连接多个点的路径的绝佳解决方案,例如绘制从点A到点B的非线性路线。
要绘制折线(polyline)
,您需要按照绘制顺序绘制一系列经度和纬度坐标。 再次在Park Information
文件夹中找到的EntranceToGoliathRoute.plist
包含路径信息。
现在,您需要一种方法来读取该plist
文件并创建供骑手遵循的路线。
首先,打开ContentView.swift
并找到addRoute()
。 然后,将其当前的TODO
实施替换为:
guard let points = Park.plist("EntranceToGoliathRoute") as? [String] else {
return
}
let cgPoints = points.map { NSCoder.cgPoint(for: $0) }
let coords = cgPoints.map { CLLocationCoordinate2D(
latitude: CLLocationDegrees($0.x),
longitude: CLLocationDegrees($0.y))
}
let myPolyline = MKPolyline(coordinates: coords, count: coords.count)
mapView.addOverlay(myPolyline)
此方法读取EntranceToGoliathRoute.plist
并将单个坐标字符串转换为CLLocationCoordinate2D
结构。
实现折线非常简单:您只需创建一个包含所有点的数组,然后将其传递给MKPolyline
! 没有比这容易的多了。
请记住,每当用户通过UI
切换此选项时,updateMapOverlayViews()
已经调用addRoute()
。 现在剩下的就是让您更新委托方法,以便它返回要在地图视图上呈现的实际视图。
返回mapView(_:rendererFor :)
并将此else if
子句添加到现有条件中:
else if overlay is MKPolyline {
let lineView = MKPolylineRenderer(overlay: overlay)
lineView.strokeColor = .green
return lineView
}
显示折线视图的过程与以前的叠加视图非常相似。 但是,在这种情况下,您无需创建任何自定义视图对象。 您只需使用提供的MKPolyLineRenderer
类并使用叠加层(overlay)
初始化一个新实例。
MKPolyLineRenderer
还可让您更改某些折线的属性。 在这种情况下,您已经修改了笔触颜色以显示为绿色。
Build
并运行您的应用程序。 启用:Route:
选项,它会显示在屏幕上:
现在,Goliath fanatics
可以在创纪录的时间内登上过山车!
最好向公园顾客显示公园边界,因为公园实际上并没有占据屏幕上显示的整个空间。
您可以使用MKPolyline
在公园边界周围绘制形状,但是MapKit
提供了另一个专门设计用于绘制闭合多边形的类:MKPolygon
。
Don’t Fence Me In: MKPolygon
MKPolygon
与MKPolyline
相似,不同之处在于坐标集中的第一个点和最后一个点相互连接以创建闭合形状。
您将创建一个MKPolygon
作为显示公园边界的叠加层。 公园边界坐标在MagicMountain.plist
中定义。 查看Park.swift
中的init(filename :)
以查看从plist
文件读取边界点的位置。
现在,在ContentView.swift
中,将addBoundary()
的TODO
实现替换为:
mapView.addOverlay(MKPolygon(
coordinates: park.boundary,
count: park.boundary.count))
给定公园实例的边界数组和点数,您可以快速轻松地创建一个新的MKPolygon
实例!
你能猜到下一步吗? 与您对MKPolyline
所做的类似。
是的,没错。 MKPolygon
像MKPolyline
一样符合MKOverlay
,因此您需要再次更新委托方法。
返回mapView(_:rendererFor :)
并将此else if
子句添加到现有条件中:
else if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = .magenta
return polygonView
}
您创建一个MKOverlayView
作为MKPolygonRenderer
的实例,并将stroke
颜色设置为洋红色。
运行应用程序并启用:Bound:
选项,以查看新边界的实际作用。 您可能需要缩小以使公园边界适合模拟器的屏幕边界。
这会考虑到折线和多边形(polylines and polygons)
。 涉及到的最后一种绘制方法是绘制圆圈作为叠加层,您将使用MKCircle
进行绘制。
Circle in the Sand: MKCircle
MKCircle
也非常类似于MKPolyline
和MKPolygon
,不同之处在于,在给定中心坐标点和确定圆弧大小的半径时,它会绘制一个圆。
许多公园游客喜欢与人物一起参观。 您可以通过用圆圈标记最后在地图上发现characters
的位置来帮助他们找到字符。 MKCircle
叠加层使您可以轻松地执行此操作。
Park Information
文件夹还包含角色位置文件。 每个文件都是由几个坐标组成的数组,用户可以在其中发现角色。
首先,在Models
组下创建一个名为Character.swift
的新swift
文件。 然后,将其内容替换为以下代码:
import MapKit
// 1
class Character: MKCircle {
// 2
private var name: String?
var color: UIColor?
// 3
convenience init(filename: String, color: UIColor) {
guard let points = Park.plist(filename) as? [String] else {
self.init()
return
}
let cgPoints = points.map { NSCoder.cgPoint(for: $0) }
let coords = cgPoints.map {
CLLocationCoordinate2D(
latitude: CLLocationDegrees($0.x),
longitude: CLLocationDegrees($0.y))
}
let randomCenter = coords[Int.random(in: 0...3)]
let randomRadius = CLLocationDistance(Int.random(in: 5...39))
self.init(center: randomCenter, radius: randomRadius)
self.name = filename
self.color = color
}
}
此代码的作用如下:
- 1)
Character
类符合MKCircle
协议。 - 2) 它定义了两个可选属性:
name
和color
。 - 3) 便捷初始化程序接受
plist
文件名和颜色来绘制圆圈。 然后,它从plist
文件中读取数据,并从文件的四个位置中选择一个随机位置。 接下来,它选择一个随机半径来模拟时间变化。 返回的MKCircle
已设置好,可以放到地图上了!
现在,您需要一种添加characters
的方法。 因此,打开ContentView.swift
并将addCharacterLocation()
的TODO
实现替换为:
mapView.addOverlay(Character(filename: "BatmanLocations", color: .blue))
mapView.addOverlay(Character(filename: "TazLocations", color: .orange))
mapView.addOverlay(Character(filename: "TweetyBirdLocations", color: .yellow))
该方法对每个character
执行几乎相同的操作:它为每个字符传递plist
文件名,确定颜色并将其作为覆盖添加到地图上。
你几乎完成! 你还记得最后一步吗?
对! 您需要使用委托方法为地图视图提供MKOverlayView
。
返回mapView(_:rendererFor :)
并将此else if
子句添加到现有条件中:
else if let character = overlay as? Character {
let circleView = MKCircleRenderer(overlay: character)
circleView.strokeColor = character.color
return circleView
}
Build
并运行该应用程序,然后启用:Characters:
选项以查看每个人都隐藏在哪里!
有许多更高级,甚至更有效的方法来创建overlays
。 例如,您可以使用KML tiles
或其他第三方提供的资源。
后记
本篇主要讲述了
Overlay Views
,感兴趣的给个赞或者关注~~~