原文: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 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?
}
你还需要能够设置公园在这个 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 个步骤:
- 创建一个自定义类,实现 MKOverlay 协议,它必须有两个属性:coordinate 和 boundingMapRect。这两个属性指明了覆盖物在地图中的位置以及大小。
- 创建一个这个类的实例,用于你想显示覆盖物的每一个地区。以这个 app 为例,你可以创建一个实例用于作为过山车的覆盖物,用另一个实例作为某个饭店的覆盖物。
- 最后,将覆盖物添加到 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: CLLocationCoordinate2D
var boundingMapRect: MKMapRect
init(park: Park) {
boundingMapRect = park.overlayBoundingMapRect
coordinate = park.midCoordinate
}
}
实现 MKOverlay 协议的同时必须继承 NSObject。最后,初始化方法直接用 Park 参数的属性赋给对应的 MKOverlay 属性。
现在你需要创建一个视图类,继承 MKOverlayRenderer。
在 Overlays 目录下新建文件 ParkMapOverlayView.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 要如何渲染这个 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 MapKit
enum AttractionType: Int {
case misc = 0
case ride
case food
case firstAid
func 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: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var type: AttractionType
init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, type: AttractionType) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.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") ?? 0
let type = AttractionType(rawValue: typeRawValue) ?? .misc
let 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 = true
return 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.green
return 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.green
return lineView
} else if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = UIColor.magenta
return 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 = filename
self.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.green
return lineView
} else if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = UIColor.magenta
return polygonView
} else if let character = overlay as? Character {
let circleView = MKCircleRenderer(overlay: character)
circleView.strokeColor = character.color
return circleView
}
return MKOverlayRenderer()
}
最后,修改 loadSelectedOptions() 方法允许用户打开/关闭人像位置:
case .mapCharacterLocation:
addCharacterLocation()
你还可以删除默认的 default: 和 break 语句,因为你已经穷举了所有 case。
Build & run,打开人像看看每个角色人物都藏在了什么地方?
接下来做什么?
恭喜你!你已经体验过 MapKit 所提供的大部分功能了。通过几个简单功能,你就编写好了一个完整可用的地图 app,包括了标注、卫星地图和自定义覆盖物。
从这里可以下载最终的示例项目。
生成覆盖物的方法有许多,可以非常简单,也可以非常复杂。在本教程采用了 overlay_park 图片的方式是一种简单——但乏味的方式。
有更好——但更高效的做法来创建覆盖物。其中一种就是使用 KML 文件,MapBox 瓦片或者其它第三方资源。
希望你喜欢这篇教程,希望在你的 app 中看到你使用 MapKit 的覆盖物。有任何问题或评论,请去论坛中发言。