MapKit框架详细解析(十三) —— MapKit Overlay Views(一)

版本记录

版本号 时间
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并运行。 您会看到以下内容:

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第1张图片

All About Overlay Views

在开始创建叠加视图(overlay views)之前,您需要了解两个关键类:MKOverlayMKOverlayRenderer

MKOverlay告诉MapKit您希望它在何处绘制叠加层。使用此类的三个步骤:

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

注意:Core Graphics绘图的详细信息不在本教程的讨论范围之内。 但是,您可以看到上面的代码使用传递的MKMapRect来获取一个CGRect,以便在其中提供在所提供的上下文中绘制图像。

很好,现在您已经有了MKOverlayMKOverlayRenderer,将它们添加到地图视图中。


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图像如下所示:

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第2张图片

构建并运行,启用屏幕顶部的:Overlay:选项,然后加油! 这是在地图顶部绘制的公园叠加层:

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第3张图片

放大,缩小并四处移动。 覆盖层会按预期缩放和移动。Cool!


Adding Annotations

如果您曾经在原生Maps应用中搜索过某个位置,那么您会看到这些彩色图钉(pins)出现在地图上。 这些是使用MKAnnotationView创建的注释(annotations)。 您可以在自己的应用程序中使用注释,并使用所需的任何图像,而不仅仅是pins`!

Annotations有助于突出显示公园游客的特定兴趣点。 它们的工作方式类似于MKOverlayMKOverlayRenderer,但是您将使用MKAnnotationMKAnnotationView

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.swiftSwift文件。 然后,将其内容替换为以下代码段:

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 :)并根据annotationtype属性设置其他注释(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:选项。

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第4张图片

在这一点上,Attraction pins看起来相当锋利!

到目前为止,您已经介绍了MapKit的一些复杂部分,包括叠加层和注释(overlays and annotations)。 但是,如果您需要使用一些绘图图元(如直线和圆)怎么办?

MapKit框架还允许您直接绘制到地图视图上。 MapKit为此提供了MKPolylineMKPolygonMKCircle。 是时候尝试一下了!


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:选项,它会显示在屏幕上:

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第5张图片

现在,Goliath fanatics可以在创纪录的时间内登上过山车!

最好向公园顾客显示公园边界,因为公园实际上并没有占据屏幕上显示的整个空间。

您可以使用MKPolyline在公园边界周围绘制形状,但是MapKit提供了另一个专门设计用于绘制闭合多边形的类:MKPolygon


Don’t Fence Me In: MKPolygon

MKPolygonMKPolyline相似,不同之处在于坐标集中的第一个点和最后一个点相互连接以创建闭合形状。

您将创建一个MKPolygon作为显示公园边界的叠加层。 公园边界坐标在MagicMountain.plist中定义。 查看Park.swift中的init(filename :)以查看从plist文件读取边界点的位置。

现在,在ContentView.swift中,将addBoundary()TODO实现替换为:

mapView.addOverlay(MKPolygon(
  coordinates: park.boundary, 
  count: park.boundary.count))

给定公园实例的边界数组和点数,您可以快速轻松地创建一个新的MKPolygon实例!

你能猜到下一步吗? 与您对MKPolyline所做的类似。

是的,没错。 MKPolygonMKPolyline一样符合MKOverlay,因此您需要再次更新委托方法。

返回mapView(_:rendererFor :)并将此else if子句添加到现有条件中:

else if overlay is MKPolygon {
  let polygonView = MKPolygonRenderer(overlay: overlay)
  polygonView.strokeColor = .magenta
  return polygonView
}

您创建一个MKOverlayView作为MKPolygonRenderer的实例,并将stroke颜色设置为洋红色。

运行应用程序并启用:Bound:选项,以查看新边界的实际作用。 您可能需要缩小以使公园边界适合模拟器的屏幕边界。

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第6张图片

这会考虑到折线和多边形(polylines and polygons)。 涉及到的最后一种绘制方法是绘制圆圈作为叠加层,您将使用MKCircle进行绘制。


Circle in the Sand: MKCircle

MKCircle也非常类似于MKPolylineMKPolygon,不同之处在于,在给定中心坐标点和确定圆弧大小的半径时,它会绘制一个圆。

许多公园游客喜欢与人物一起参观。 您可以通过用圆圈标记最后在地图上发现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) 它定义了两个可选属性:namecolor
  • 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:选项以查看每个人都隐藏在哪里!

MapKit框架详细解析(十三) —— MapKit Overlay Views(一)_第7张图片

有许多更高级,甚至更有效的方法来创建overlays。 例如,您可以使用KML tiles或其他第三方提供的资源。

后记

本篇主要讲述了Overlay Views,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(MapKit框架详细解析(十三) —— MapKit Overlay Views(一))