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

版本记录

版本号 时间
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选项,瞧! 在地图上方绘制了公园覆盖图:

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

后记

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容