原文:MapKit Tutorial: Overlay Views
作者:Owen Brown
译者:kmyhy

更新说明:本教程由 Owen Brown 更新至 Xcode 9、iOS 11 和 Swift 4。原文作者是 Chris Wagner。

苹果通过 MapKit 让你轻易地将地图添加到你的 app,只是它看起来并不是那么漂亮。幸运的是,你可以用自定义覆盖物让它更引人注目。

在这篇 MapKit 教程中,你将创建一个展示六旗魔术山的 app。如果你是一个寻求刺激的快车手,这个 app 就是专属于你的了!

完成这个 app 之后,你将有一个互动式公园地图,显示出景点位置、骑乘路线和人物位置。

开始

从这里下载开始项目。这个项目已经包含了导航,但没有地图。

打开开始项目,Build & run,你会看到一个空白页面。你会在上面添加一个地图并允许选择覆盖物的类型。

添加 MapKit 中的 MapView

打开 Main.storyboard 选择 Park Map View Controller 场景。

从 Object Library 中拖一个 Map View 到这个场景中。将它放到导航条下面占满剩余的空间。

然后,点击 Add New Constraints 按钮,添加 4 个约束,constant 都设置为 0,点击 Add 4 Constraints。

绑定 MapView

要使用这个 MapView,你必须做两件事情:(1) 为它创建一个出口 (2) 设置它的 delegate。

在 file 视图中用 alt+鼠标左键点击 ParkMapViewController.swift,在助手编辑器中打开 ParkMapViewController。

然后,从 map view 拖一条线到右边:

在弹出窗口中,将出口命名为 mapView,然后点击 Connect。

要设置 map view 的委托,右键点击 map view 对象,打开上下文菜单,然后从其中的 delegate 出口拖到 Park Map View Controller:

你还必须让 ParkMapViewController 适应 MKMapViewDelegate 协议。

首先,为 ParkMapViewController 添加相应的 import 语句:

import MapKit

然后,在类声明关闭的大括号后面添加一个扩展:

extension ParkMapViewController: MKMapViewDelegate {}

Build & run,看看你的新地图:

如果不用这个地图真正地做些什么事情,那有什么意思呢?是时候和地图进行一些交互了!

和地图交互

首先是让地图中心指向这个公园。在 app 的 Park Information 文件夹中,你会发现一个 MagicMountain.plist 的文件。打开这个文件,你会看到它有一个坐标,包含了公园的中心和边界信息。

现在创建一个模型。用于表示这个 plist,以便 app 使用。

右键点击文件导航器中的 Models 文件夹,选择 New File…,选择 iOS\Source\Swift File 模板并命名为 Park.swift。编辑内容为:

import UIKit
import MapKitclass 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?
}

你还需要能够设置公园在这个 plist 文件中的数据。

首先,添加一个实用方法,用于反序列化 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 和一个字典对象来解析 CLLocationCorrdinate2D :


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 使用 CLLocationCoordinate 2D 来表示一个地理坐标。

现在可以创建这个类的初始化方法了:

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 文件中获取,然后赋给 properties 变量。然后设置边界数组,以便后面用来显示公园的框线。

你可能奇怪,为什么不用 plist 来设置 overlayBottomRightCoordinate 坐标?这个不需要在 plist 中提供,因为你可以通过其它 3 个点来算出。将当前的 overlayBottomRightCoordinate 替换成一个计算属性:

var overlayBottomRightCoordinate: CLLocationCoordinate2D {get {return CLLocationCoordinate2DMake(overlayBottomLeftCoordinate.latitude,overlayTopRightCoordinate.longitude)}
}

最后,你需要写一个方法,根据 overlay 坐标创建一个矩形边框。

将 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// 将一个 span 想象成电视机的尺码,用对角线长度来测量let span = MKCoordinateSpanMake(fabs(latDelta), 0.0)let region = MKCoordinateRegionMake(park.midCoordinate, span)mapView.region = region
}

这里创建了一个纬度差,它是从公园左上角的坐标到公园有效较的坐标的距离。你用它来创建一个 MKCoordinateSpan,这在地图上表示了一小块横跨区域。然后用这个 span 和公园的中心坐标创建一个 MKCoordinateRegion,然后将 map view 的定位到这个公园的位置。

Build & run,你会看到现在地图中央已经是六旗魔术山了!

OK!你已经将公园放在地图中心,这很好,但也不用那么激动。让我们将地图类型修改为卫星视图!

切换地图类型

在 ParkMapViewController.swift 中,你应该看到这个方法:

@IBAction func mapTypeChanged(_ sender: UISegmentedControl) {// TODO
}

呃,这里有一个很不好的注释~

幸好,开始项目已经有许多你需要在这个方法中用到的东西了。看到 map view 上面的那个好像毫无用处的 segmented 控件了吗?

这个 segmented 框架实际上会调用 mapTypeChanged(_:) 方法,但从上面看来,它还没有任何代码!

将下列实现填充到 mapTypeChanged() 方法中来:

mapView.mapType = MKMapType.init(rawValue: UInt(sender.selectedSegmentIndex)) ?? .standard

无论你相信与否,在你的 app 中添加标准地图、卫星地图和混合地图类型就是这么简单!

Build & run,点一下 segmented 控件就可以修改地图类型!

尽管卫星地图比标准地图要好看许多,对于一个公园游客来说也没什么鸟用。什么标注都没有——你的用户怎么找得到公园中的地点呢?

一个简单的方法就是在 map view 上覆盖一个 UIView,但你可以更进一步,用 MKOverlayRenderer 来为你做大量的工作!

关于 Overlay View 的一切

在开始编写自己的 overlay view 之前,来看两个关键的类:MKOverlay和 MKOverlayRenderer。

MKOverlay 告诉 MapKit 你想将覆盖物画在哪儿。这个类的使用分成 3 个步骤:

  1. 创建一个自定义类,实现 MKOverlay 协议,它必须有两个属性:coordinate 和 boundingMapRect。这两个属性指明了覆盖物在地图中的位置以及大小。
  2. 创建一个这个类的实例,用于你想显示覆盖物的每一个地区。以这个 app 为例,你可以创建一个实例用于作为过山车的覆盖物,用另一个实例作为某个饭店的覆盖物。
  3. 最后,将覆盖物添加到 map view。

现在 map view 知道它需要在哪个地方显示覆盖物,但它不知道在每个地方需要显示什么?

再来看看 MKOverlayRenderer。这个子类用于说明你想在一个地点上显示些什么。以这个 app 为例,你会画一张过山车或者饭店的图片。

一个 MKOverlayRenderer 就像一个特殊的 UIView,因为它继承了 UIView,但是,你不需要直接将一个 MKOverlayRenderer 添加到 mapview。相反,MapKit 需要的是一个 MKMapView。

还记得之前你设置了 map view 的委托吗?它有一个委托方法允许你返回一个 overlay view:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer

MapKit 会在 map view 在显示时觉得某个区域需要一个 MKOverlay 时调用这个方法。

总之,你不直接添加 MKOverlayRenderer 对象到 map view;而是告诉地图有 MKOverlay 对象会显示,然后在委托方法需要使用它们的时候返回它们。

现在你已经有理论基础了,接下来使用这些理论吧!

添加你自己的信息

就如你前面所看到的,卫星地图也不会提供关于公园的足够的信息。你的任务是创建一个表示整个公园的一个覆盖物对象。

在 Overlays 文件夹下新建文件 ParkMapOverlay.swift。编辑内容如下:

import UIKit
import MapKit

class ParkMapOverlay: NSObject, MKOverlay {var coordinate: CLLocationCoordinate2Dvar boundingMapRect: MKMapRectinit(park: Park) {boundingMapRect = park.overlayBoundingMapRectcoordinate = park.midCoordinate}
}

实现 MKOverlay 协议的同时必须继承 NSObject。最后,初始化方法直接用 Park 参数的属性赋给对应的 MKOverlay 属性。

现在你需要创建一个视图类,继承 MKOverlayRenderer。

在 Overlays 目录下新建文件 ParkMapOverlayView.swift。编辑内容为:

import UIKit
import MapKit

class ParkMapOverlayView: MKOverlayRenderer {var overlayImage: UIImageinit(overlay:MKOverlay, overlayImage:UIImage) {self.overlayImage = overlayImagesuper.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 要如何渲染这个 view,同时指定一个 MKMapRect、MKZoomScale 和 图形上下文 CGContext,也就是用对应的缩放比例将覆盖图片会知道该上下文中。

Core Graphics 的绘图细节超出了本文范围。但是,你可以在上面的代码中看到,它使用了传入的 MKMapRect 来获取 CGRect,为的是在指定上下文中将 UIImage 的 CGImage 绘制在指定的地方。如果你想学习更多 Core Graphics 内容,请阅读我们的 Core Graphics 教程系列。

好了!现在我们有了 MKOverlay 也有了 MKOverlayRenderer,可以将它们添加到 map view 中了。

在 ParkMapViewController.swift 中,添加方法:

func addOverlay() {let overlay = ParkMapOverlay(park: park)mapView.add(overlay)
}

这个方法会添加一个 MKOverlay 到 map view。

如果用户选择显示覆盖物,loadSelectedOptions() 会调用 addOverlay()。将 loadSeletedOptions() 替换为:

func loadSelectedOptions() {mapView.removeAnnotations(mapView.annotations)mapView.removeOverlays(mapView.overlays)for option in selectedOptions {switch (option) {case .mapOverlay:addOverlay()default:break;}}
}

当用户解散选择选项视图,app 会调用 loadSelectedOptions(),然后判断用户选定的选项并调用对应的方法将这些选项显示在 map view。

loadSelectedOptions() 还会删除所有大头钉和覆盖物,以避免重复绘制。这样的效率确实不高,但却是一种简单的清除上一次地图内容的方法。

在 MKMapViewDelegate 扩展中添加下列方法以实现该委托方法:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {if overlay is ParkMapOverlay {return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))} return MKOverlayRenderer()
}

当 app 发现视图中有一个 MKOverlay 时,map view 会调用上面的委托方法。

在这个方法中,你判断了 overlay 的类型是否是 ParkMapOverlay,如果是,加载这个 overlay 图片,将这个对象返回给调用者。

有一个地方忘记说了——这个 overlay_park 图片是哪来的?

这是一个 PNG 文件,目的是盖在 map view 上作为公园的边界。overlay_park 图片(在 image assets 中)大概是这个样子:

Build & run,选择 Map Overlay 选项,啊!在地图上出现了一个公园的样子:

放大,缩小,随意移动——这个覆盖物会随之缩放和移动。太棒了!

标注

如果你曾经在地图 app 中搜索过地名,你肯定见过地图中显示的彩色图钉。这就是标注,用 MKAnnotationView 创建的。你可以在自己的 app 使用标注——你可以使用任意图片而不仅仅是图钉。

在你的 app 中也会用到标注,为游客指出某些景点。标注对象和 MKOverlay 和 MKOverlayRenderer 类似,但使用的是 MKAnnotation 和 MKAnnotationView。

在 Annotations 文件夹中新建一个 Swift 文件 AttractionAnnotation.swift,编辑内容如下:

import UIKit
import MapKitenum AttractionType: Int {case misc = 0case ridecase foodcase firstAidfunc image() -> UIImage {switch self {case .misc:return #imageLiteral(resourceName: "star")case .ride:return #imageLiteral(resourceName: "ride")case .food:return #imageLiteral(resourceName: "food")case .firstAid:return #imageLiteral(resourceName: "firstaid")}}
}class AttractionAnnotation: NSObject, MKAnnotation {var coordinate: CLLocationCoordinate2Dvar title: String?var subtitle: String?var type: AttractionTypeinit(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, type: AttractionType) {self.coordinate = coordinateself.title = titleself.subtitle = subtitleself.type = type}
}

首先定义了一个枚举 AttractionType,用于区分景点类型。这个枚举列出了标注的四种类型:misc、rides、foods 和 first aid。还有一个函数用于返回对应的标注图片。

然后声明本类使用了 MKAnnotation 协议。和 MKOverlay 一样,MKAnnotation 也有一个必须的 coordinate 属性。在类中定义了一些属性。最后定义了一个初始化函数,允许你直接对这些属性初始化。

现在必须创建一个 MKAnnotation 实例以用于你的标注。

在 Annotations 文件夹中再新建一个文件 AttractionAnnotationView.swift,编辑内容为:

import UIKit
import MapKit

class AttractionAnnotationView: MKAnnotationView {// 必须实现required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder)}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()}
}

MKAnnotationView 必须实现 init(coder:) 初始化函数。如果不实现这个,会出现编译错误。为此,简单地定义这个方法并调用父类初始化函数。这里,你还覆盖了 init(annotation:resuseIdentifier:) 方法,根据标注的类型,设置不同的图片给这个标注的 image 属性。

创建完标注和对应的 view,你可以将它们添加到 map view 中了!

要获得每个标注的坐标,你可以使用 MagicMountainAttractions.plist 文件,它位于 Park Information 文件夹下面。这个 plist 文件包含了公园景点的坐标和相关信息。

回到 ParkMapViewController.swift,添加方法:

func addAttractionPins() {guard let attractions = Park.plist("MagicMountainAttractions") as? [[String : String]] else { return }for attraction in attractions {let coordinate = Park.parseCoord(dict: attraction, fieldName: "location")let title = attraction["name"] ?? ""let typeRawValue = Int(attraction["type"] ?? "0") ?? 0let type = AttractionType(rawValue: typeRawValue) ?? .misclet subtitle = attraction["subtitle"] ?? ""let annotation = AttractionAnnotation(coordinate: coordinate, title: title, subtitle: subtitle, type: type)mapView.addAnnotation(annotation)}
}

这个方法读取了 MagicMoutainAttractions.plist,遍历数组中的每个字典。对于每个字典,它都会创建一个 AttractionAnnotation 实例,设置这个景点的信息,然后添加到 map view。

现在你需要根据新选项修改 loadSelectedOptions() 方法,当用于选择它时,执行你的新方法。

修改 loadSelectedOptions() 方法中的 switch 语句:

case .mapPins:addAttractionPins()

这会在必要的时候调用你的 addAttractionPins() 方法。注意 removeOverlays 一句会隐藏覆盖物。

快完了!但还有一点,你必须实现另一个委托方法,为 map view 提供 MKAnnotationView 对象,这样它才能画出它们。

在 MKMapViewDelegate 类扩展中添加方法:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {let annotationView = AttractionAnnotationView(annotation: annotation, reuseIdentifier: "Attraction")annotationView.canShowCallout = truereturn annotationView
}

这个方法接收指定的 MKAnnotation,用它创建 AttractionAnnotationView。因为 canShowCallout 设置为 true,当用户点击这些标注时会弹出一个气泡。最后,方法返回了 annotation view。

Build & run,看看你的标注吧!

打开 Attraction Pins 选项,效果如下图所示:

这些标注显得非常“显眼”!

到此为止,你已经学习了许多 MapKit 的高级内容,比如覆盖物和标注。但如果有时候必须绘制几何图形怎么办?比如线、形状和圆?

MapKit 也允许你在 map view 上直接绘图。MapKit 提供了 MKPolyline、MKPolygon、MKCircle。

我一往无前——MKPolyline

如果你去过迷幻山,你会知道歌利亚过山车非常陡峭,有些乘客在一进门的时候就会朝它直奔而去!

为了帮助这些乘客,你需要从入口绘制一条路线到达歌利亚。

MKPolyline 是一种绘制路线的好方法,它会连接多个点,比如从 A 点到 B 点绘制一条非线性的路线。

要绘制多线段,你需要有一系列经纬度坐标,以便代码能够绘制出它们。

EntranceToGoliathRoute.plist(还是在 Park Information 文件夹)包含了这些路径信息。

你需要有一种方法读取这个 plist 并创建一条路径,为乘客们导航。

打开 ParkMapViewController.swift,添加方法:

func addRoute() {guard let points = Park.plist("EntranceToGoliathRoute") as? [String] else { return }let cgPoints = points.map { CGPointFromString($0) }let coords = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }let myPolyline = MKPolyline(coordinates: coords, count: coords.count)mapView.add(myPolyline)
}

这个方法读取了 EntranceToGoliathRoute.plist,将每个坐标字符串转换成 CLLocationCoordinate2D 结构。

值得注意的是,在 app 实现你的多线段非常简单,直接创建一个包含了所有点的数组,把它传递给 KMPolyline 就可!还有什么比这个更简单的呢?

然后你需要添加一个选项,允许用户打开或关闭多线段。

修改 loadSelectedOptions() 方法,添加另一个 case 语句:

case .mapRoute:addRoute()

适时地调用了 addRoute() 方法。

最后将所有代码串接在一起。你必须修改委托方法,以便返回 map view 要用到的 view。

修改 mapView(_:rendererForOverlay) 方法:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {if overlay is ParkMapOverlay {return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))} else if overlay is MKPolyline {let lineView = MKPolylineRenderer(overlay: overlay)lineView.strokeColor = UIColor.greenreturn lineView}return MKOverlayRenderer()
}

要改的地方是多出来的 else if 语句,这是针对 MKPolyline 对象的。显示 polyline 的过程非常类似于 overlay 的过程。但是,这里不需要创建任何自定义的视图对象。直接用框架提供的 MKPolylineRenderer,用 overlay 初始化一个新的示例。

MKPolylineRenderer 也允许你修改多线段的某些属性。在这里,你修改了绘制线段的颜色为绿色。

Build & run,打开 Route 选项,它会显示成:

歌利亚迷现在可以用前所未有的速度来找到过山车了!

在公园真实旁边显示公园的资助人也挺好呀,因为公园并没有完全占满整个屏幕。

虽然你可以用 MKPolyline 绘制公园边界,但 MapKit 有另外一个类专门用于绘制封闭的多边形:MKPolygon。

别挡住我进去——MKPolygon

MKPolygon 和 MKPolyline 非常类似,只不过在它的坐标集中,起点和终点是连接的,创建出来的是一个封闭图形。

你可以创建一个 MKPolygon 用于表示公园的边界。公园边界坐标已经定义在 MagicMountain.plist 中,回去看一眼 init(filename:) 方法,是从 plist 文件的什么地方获取的。

在 ParkMapViewController.swift 中添加方法:

func addBoundary() {mapView.add(MKPolygon(coordinates: park.boundary, count: park.boundary.count))
}

这个方法非常简单。通过读取 park 对象的 boundary 数组和端点数,你可以快速和轻易地创建一个 MKPolygon 对象!

接下来要做什么呢?和上面的 MKPolyline 非常类型。

耶,对了——在 loadSelectedOptions 中添加另外一个 case 语句,以便显示/隐藏公园的边界:

case .mapBoundary:addBoundary()

和 MKPolyline 一样 MKPolygon 也使用了 MKOverlay 协议,因此你必须再次修改 delegate 方法。

在 ParkMapViewController.swift 将委托方法修改为:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {if overlay is ParkMapOverlay {return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))} else if overlay is MKPolyline {let lineView = MKPolylineRenderer(overlay: overlay)lineView.strokeColor = UIColor.greenreturn lineView} else if overlay is MKPolygon {let polygonView = MKPolygonRenderer(overlay: overlay)polygonView.strokeColor = UIColor.magentareturn polygonView}return MKOverlayRenderer()
}

要改的地方和之前类型。你创建了一个 MKPolygonRenderer 对象的 MKOverlayView,然后设置它的绘制颜色为品红色。

运行app 是这个样子:

这就是多线段和多边形。最后一个要绘制的图形是圆形,也就是 MKCircle。

沙滩上的圆圈——MKCircle

MKCircle 和 MKPolyline 和 MKPolygon 还是很像,只不过它画的是圆,需要指定一个坐标点作为它的圆心,以及一个半径指定圆的大小。

将每个公园的人像的常见位置标记出来也很好啊!将这些人像在地图上用小圆点画出来。

MKCircle 就能非常轻易地实现这个功能。

在 Park Information 文件夹包含了这些人物的坐标文件。每个文件是一个数组,包含了用户看见过的人像坐标。

在 Models 文件夹下新建一个文件 Character.swift。编辑内容为:

import UIKit
import MapKit

class Character: MKCircle {var name: String?var color: UIColor?convenience init(filename: String, color: UIColor) {guard let points = Park.plist(filename) as? [String] else { self.init(); return }let cgPoints = points.map { CGPointFromString($0) }let coords = cgPoints.map { CLLocationCoordinate2DMake(CLLocationDegrees($0.x), CLLocationDegrees($0.y)) }let randomCenter = coords[Int(arc4random()%4)]let randomRadius = CLLocationDistance(max(5, Int(arc4random()%40)))self.init(center: randomCenter, radius: randomRadius)self.name = filenameself.color = color}
}

这个类实现了 MKCircle 协议,定义了两个可空属性:名字和颜色。这个便利初始化方法有一个 plist 文件名和一个颜色作为参数用于绘制圆点。然后从 plist 文件中读取数据,然后从文件中的 4 个坐标中选择一个随机的坐标。然后设置 MKCircle 并返回,准备放到地图中去吧!

现在你需要一个添加每个人像的方法。打开 ParkMapViewController.swift 添加方法:

func addCharacterLocation() {mapView.add(Character(filename: "BatmanLocations", color: .blue))mapView.add(Character(filename: "TazLocations", color: .orange))mapView.add(Character(filename: "TweetyBirdLocations", color: .yellow))
}

这个方法在每个人像进行了同样的操作。每个都传入一个 plist 文件名和颜色,然后在地图上添加一个覆盖物。

快大功告成了!你还能想起后续的步骤吗?

对,你仍然要给 map view 提供 MKOverlayView,这是通过委托方法来进行的。

修改 ParkMapViewController.swift 中的委托方法:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {if overlay is ParkMapOverlay {return ParkMapOverlayView(overlay: overlay, overlayImage: #imageLiteral(resourceName: "overlay_park"))} else if overlay is MKPolyline {let lineView = MKPolylineRenderer(overlay: overlay)lineView.strokeColor = UIColor.greenreturn lineView} else if overlay is MKPolygon {let polygonView = MKPolygonRenderer(overlay: overlay)polygonView.strokeColor = UIColor.magentareturn polygonView} else if let character = overlay as? Character {let circleView = MKCircleRenderer(overlay: character)circleView.strokeColor = character.colorreturn circleView}return MKOverlayRenderer()
}

最后,修改 loadSelectedOptions() 方法允许用户打开/关闭人像位置:

case .mapCharacterLocation:addCharacterLocation()

你还可以删除默认的 default: 和 break 语句,因为你已经穷举了所有 case。

Build & run,打开人像看看每个角色人物都藏在了什么地方?

接下来做什么?

恭喜你!你已经体验过 MapKit 所提供的大部分功能了。通过几个简单功能,你就编写好了一个完整可用的地图 app,包括了标注、卫星地图和自定义覆盖物。

从这里可以下载最终的示例项目。

生成覆盖物的方法有许多,可以非常简单,也可以非常复杂。在本教程采用了 overlay_park 图片的方式是一种简单——但乏味的方式。

有更好——但更高效的做法来创建覆盖物。其中一种就是使用 KML 文件,MapBox 瓦片或者其它第三方资源。

希望你喜欢这篇教程,希望在你的 app 中看到你使用 MapKit 的覆盖物。有任何问题或评论,请去论坛中发言。

MapKit 教程: 覆盖物相关推荐

  1. MapKit 进阶教程: 自定义瓦片

    原文:Advanced MapKit Tutorial: Custom Tiles 作者:Michael Katz 译者:kmyhy 在现代 app 中,地图的使用无处不在.地图可以提供 POI(兴趣 ...

  2. 高德地图的标志放大_点标记-覆盖物-教程-地图 JS API | 高德地图API

    点标记是用来标示某个位置点信息的一种地图要素,本章介绍如何在地图图面使用点标记,分别包括:点标记 Marker 灵活点标记 ElasticMarker 圆形标记 CircleMarker 文本标记 T ...

  3. 高德地图怎么画圈_点标记-覆盖物-教程-地图 JS API | 高德地图API

    点标记是用来标示某个位置点信息的一种地图要素,本章介绍如何在地图图面使用点标记,分别包括:点标记 Marker 灵活点标记 ElasticMarker 圆形标记 CircleMarker 文本标记 T ...

  4. js添加多marker 高德地图_点标记-覆盖物-教程-地图 JS API | 高德地图API

    点标记是用来标示某个位置点信息的一种地图要素,本章介绍如何在地图图面使用点标记,分别包括:点标记 Marker 灵活点标记 ElasticMarker 圆形标记 CircleMarker 文本标记 T ...

  5. SwiftUI Mapkit 导航基础教程大全之 MKMapView地图显示并实现导航线路(教程含源码)

    实战需求 MKMapView地图显示并实现导航线路 本文价值与收获 看完本文后,您将能够作出下面的界面 MKMapView 是什么 一种可嵌入的地图界面,类似于地图应用程序提供的界面.您可以将地图以给 ...

  6. 【iOS】Mapkit的使用:地图显示、定位、大头针、气泡等

    以前做项目用高德地图SDK,需要注册账号和AppID,然后下载SDK集成到项目中,比较麻烦,这几天看了下苹果自带的MapKit框架,感觉挺好用,官方文档也介绍得很详细,所以按照官方文档写了个demo, ...

  7. Cesium中级教程9 - Advanced Particle System Effects 高级粒子系统效应

    Cesium中文网:http://cesiumcn.org/ | 国内快速访问:http://cesium.coinidea.com/ 要了解粒子系统的基础知识,请参见粒子系统入门教程. Weathe ...

  8. 【新手教程】如何向App Store提交应用

    作者:Bart Jacobs 当你克服重重困难终于开发出了自己的App,下一步就是向App Store提交应用了,这时应该如何操作呢?我的App真的准备好提交了?我敢肯定这些问题将会浮现在你的脑海.基 ...

  9. 【HIMI转载推荐之三】新手教程之如何向APP STORE提交应用

    当你克服重重困难终于开发出了自己的App,下一步就是向App Store提交应用了,这时应该如何操作呢?我的App真的准备好提交了?我敢肯定这些问题将会浮现在你的脑海.基于这篇教程,我将告诉你一个完整 ...

最新文章

  1. HTML5跳转页面并传值以及localStorage的用法
  2. 法猿生存计划--左边的管理,技术正确
  3. 互联网1分钟 |1224
  4. 20145240 《信息安全系统设计基础》第三周学习总结
  5. 计算机 ieee access,计算机 | IEEE Access 诚邀专刊稿件 (IF:3.557)
  6. .NET中操作SQLite
  7. Jsp 【项目路径】
  8. java案例代码6-评委打分的模拟程序
  9. 超定方程组的经典Gram-Schmidt正交化解法
  10. Linux设备驱动程序 之 中断和锁
  11. python建模的步骤_python基础教程之Python 建模步骤|python基础教程|python入门|python教程...
  12. 01-解决Tomcat中文乱码问题
  13. 蓝桥杯web开发-5道模拟题让你信心满满
  14. Hive修改分区表的分区列类型
  15. 我对INFOR WMS实施的一些感想
  16. Windows驱动开发WDM (13)- 过滤驱动
  17. Jenkins 如何与 Kubernetes 集群的 Tekton Pipeline 交互?
  18. 汽车网络安全风口渐起,诚迈科技与Trustonic牵手“发力”
  19. 狂刷《Java权威面试指南(阿里版)》,助你一臂之力,事半功倍
  20. Suspicious Operation Django

热门文章

  1. Python要如何实现列表排序的几种方法
  2. 在线音视频流测试地址记录
  3. 【C语言】链表的一般设计步骤
  4. win10计算机休眠设置在哪里,一招教你找回Win10休眠功能 Win10电脑没有休眠怎么回事?...
  5. STM32学习笔记4——HC_SR04超声波测距模块的调试记录
  6. Streaming Data Ingest介绍
  7. java 全局 map_java中map 9种常规用法
  8. Android 快速绘制一个圆角矩形
  9. PHP程序员开发工具
  10. 使用torchserve时出错:RESOURCE_EXHAUSTED: gRPC message exceeds maximum size 6553500: 10838210