美文网首页iOS 深度好文
MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例

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

作者: 刀客传奇 | 来源:发表于2018-10-14 15:49 被阅读0次

    版本记录

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

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

    后记

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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/yjizaftx.html