MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例(一)

96
刀客传奇
2018.10.14 15:49 字数 2982

版本记录

版本号 时间
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 - 所有关于叠加视图

在开始创建自己的叠加视图之前,您需要了解两个关键类:MKOverlayMKOverlayRenderer

MKOverlay告诉MapKit你想要绘制叠加层的位置。使用该类有三个步骤:

  • 1) 创建自己的实现MKOverlay protocol协议的自定义类,该协议具有两个必需属性:coordinateboundingMapRect。这些属性定义了叠加层在地图上的位置以及叠加层的大小。
  • 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在给定特定的MKMapRectMKZoomScale和图形上下文的CGContext时应如何呈现此视图,以便以适当的比例将叠加图像绘制到上下文中。

Core Graphics绘图的详细信息远远超出了本教程的范围。 但是,您可以看到上面的代码使用传递的MKMapRect来获取CGRect,以便确定在提供的上下文中绘制UIImageCGImage的位置。

现在您已同时拥有MKOverlayMKOverlayRenderer,您可以将它们添加到地图视图中。

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()还会删除可能存在的任何annotationsoverlays,以便您不会最终出现重复的渲染。 这不一定有效,但它是从地图中清除先前项目的简单方法。

要实现代理方法,请将以下方法添加到文件底部的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选项,瞧! 在地图上方绘制了公园覆盖图:

根据需要放大,缩小和移动 - 叠加视图按照您的预期进行缩放和移动。

后记

本篇主要讲述了一个叠加视图相关的简单示例,感兴趣的给个赞或者关注~~~

OC
Gupao