MapKit框架詳細解析(四) —— 一個疊加視圖相關的簡單示例(一)

版本記錄

版本號 時間
V1.0 2018.10.14 星期日

前言

MapKit框架直接從您的應用界面顯示地圖或衛(wèi)星圖像,調(diào)出興趣點,并確定地圖坐標的地標信息。接下來幾篇我們就一起看一下這個框架。感興趣的看下面幾篇文章。
1. MapKit框架詳細解析(一) —— 基本概覽(一)
2. MapKit框架詳細解析(二) —— 基本使用簡單示例(一)
3. MapKit框架詳細解析(三) —— 基本使用簡單示例(二)

開始

首先看一下寫作環(huán)境

Swift 4, iOS 11, Xcode 9

本篇主要就是了解如何使用MapKit疊加視圖將衛(wèi)星和混合地圖,自定義圖像,注釋,線條,邊界和圓圈添加到標準地圖。

Apple可以很容易地使用MapKit向您的應用添加地圖,但僅此一點并不十分吸引人。 幸運的是,您可以使用custom overlay views使地圖更具吸引力。

在這個MapKit教程中,您將創(chuàng)建一個應用程序來展示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執(zhí)行任何有用的操作,您需要做兩件事:(1)為其設置outlet,以及(2)設置其代理。

通過按住Option鍵并在文件層次結構中左鍵單擊ParkMapViewController.swift,在Assistant Editor中打開ParkMapViewController。

然后,從map view按住control拖動到第一個方法的正上方,如下所示:

在出現(xiàn)的彈出窗口中,將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的文件。 打開此文件,您將看到它包含公園中點和邊界信息的坐標。

您現(xiàn)在將為此plist創(chuàng)建一個模型,以便在應用程序中輕松使用。

右鍵單擊文件導航中的Models組,然后選擇New File ...,選擇iOS \ Source \ Swift File模板并將其命名為Park.swift。 用以下內(nèi)容替換其內(nèi)容:

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來表示地理位置。

您現(xiàn)在終于準備為此類創(chuàng)建初始化程序:

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數(shù)組,稍后您將使用它來顯示公園輪廓。

您可能想知道,“為什么沒有從plist設置overlayBottomRightCoordinate?”這在plist中沒有提供,因為您可以從其他三個點輕松計算它。

用這個計算屬性替換當前的overlayBottomRightCoordinate

var overlayBottomRightCoordinate: CLLocationCoordinate2D {
  get {
    return CLLocationCoordinate2DMake(overlayBottomLeftCoordinate.latitude,
                                      overlayTopRightCoordinate.longitude)
  }
}

最后,您需要一種方法來基于疊加坐標創(chuàng)建邊界框。

用這個替換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對象。 這只是一個矩形,它定義了公園的大小,以公園的中點為中心。

現(xiàn)在是時候讓這個類使用了。 打開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
}

這將創(chuàng)建一個緯度增量,即從公園的左上角坐標到公園的右下角坐標的距離。 您可以使用它來生成MKCoordinateSpan,它定義了地圖區(qū)域所跨越的區(qū)域。 然后使用MKCoordinateSpan和公園的midCoordinate創(chuàng)建一個MKCoordinateRegion,將公園定位在地圖視圖上。

Build并運行您的應用程序,您將看到地圖現(xiàn)在以Six Flags Magic Mountain為中心!

好的! 你把地圖集中在以公園為中心,這很不錯,但并不是非常令人興奮。 讓我們通過將地圖類型切換為衛(wèi)星來增添趣味!


Switching The Map Type - 切換地圖類型

ParkMapViewController.swift中,您會注意到這個方法:

@IBAction func mapTypeChanged(_ sender: UISegmentedControl) {
  // TODO
}

入門項目有很多你需要做的來充實這個方法。 您是否注意到位于地圖視圖上方的segmented control似乎做了很多事情?

segmented control實際上是調(diào)用mapTypeChanged(_ :),但正如你在上面看到的,這個方法什么也沒做!

將以下實現(xiàn)添加到mapTypeChanged()

mapView.mapType = MKMapType.init(rawValue: UInt(sender.selectedSegmentIndex)) ?? .standard

信不信由你,在您的應用中添加標準,衛(wèi)星和混合地圖類型就像上面的代碼一樣簡單! 那不容易嗎?

Build并運行,并嘗試分段控件來更改地圖類型!

即使衛(wèi)星視圖仍然比標準地圖視圖好得多,它對您的公園訪客仍然沒有多大幫助。 沒有任何標簽 - 您的用戶將如何在公園內(nèi)找到任何東西?

一個顯而易見的方法是將UIView放在地圖視圖的頂部,但是你可以更進一步,而是利用MKOverlayRenderer的魔力為你做很多工作!


All About Overlay Views - 所有關于疊加視圖

在開始創(chuàng)建自己的疊加視圖之前,您需要了解兩個關鍵類:MKOverlayMKOverlayRenderer

MKOverlay告訴MapKit你想要繪制疊加層的位置。使用該類有三個步驟:

  • 1) 創(chuàng)建自己的實現(xiàn)MKOverlay protocol協(xié)議的自定義類,該協(xié)議具有兩個必需屬性:coordinateboundingMapRect。這些屬性定義了疊加層在地圖上的位置以及疊加層的大小。
  • 2) 為要顯示疊加層的每個區(qū)域創(chuàng)建類的實例。例如,在這個應用程序中,您可以為過山車覆蓋層創(chuàng)建一個實例,為餐廳覆蓋層創(chuàng)建另一個實例。
  • 3) 最后,將疊加層添加到地圖視圖中。

現(xiàn)在,地圖視圖知道它應該顯示疊加的位置,但它如何知道每個區(qū)域中顯示的內(nèi)容?

輸入MKOverlayRenderer。您將其子類化以設置要在每個點中顯示的內(nèi)容。例如,在這個應用程序中,您將繪制過山車或餐廳的圖像。

MKOverlayRenderer實際上只是一種特殊的UIView,因為它繼承自UIView。但是,您不應將MKOverlayRenderer直接添加到MKMapView。相反,MapKit希望這是一個MKMapView。

還記得你之前設置的地圖視圖代理嗎?有一個代理方法,允許您返回疊加視圖:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer

當MapKit意識到地圖視圖正在顯示的區(qū)域中有一個MKOverlay對象時,它將調(diào)用此方法。

總結一下,不要將MKOverlayRenderer對象直接添加到地圖視圖中;相反,您告訴地圖有關MKOverlay對象的顯示,并在代理方法請求它們時返回它們。

既然您已經(jīng)了解了這個理論,那么現(xiàn)在是時候使用這些概念了!


Adding Your Own Information - 添加自己的信息

如前所述,衛(wèi)星視圖仍未提供有關公園的足夠信息。 您的任務是創(chuàng)建一個表示整個公園的疊加層的對象。

選擇Overlays組并創(chuàng)建一個名為ParkMapOverlay.swift的新Swift文件。 用以下內(nèi)容替換其內(nèi)容:

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屬性。

現(xiàn)在,您需要創(chuàng)建一個從MKOverlayRenderer類派生的視圖類。

Overlays組中創(chuàng)建一個名為ParkMapOverlayView.swift的新Swift文件。 用以下內(nèi)容替換其內(nèi)容:

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 :)通過提供第二個參數(shù)有效地覆蓋了基本方法init(overlay :)

draw是這堂課的真正做東西的地方。 它定義了MapKit在給定特定的MKMapRectMKZoomScale和圖形上下文的CGContext時應如何呈現(xiàn)此視圖,以便以適當?shù)谋壤龑B加圖像繪制到上下文中。

Core Graphics繪圖的詳細信息遠遠超出了本教程的范圍。 但是,您可以看到上面的代碼使用傳遞的MKMapRect來獲取CGRect,以便確定在提供的上下文中繪制UIImageCGImage的位置。

現(xiàn)在您已同時擁有MKOverlayMKOverlayRenderer,您可以將它們添加到地圖視圖中。

ParkMapViewController.swift中,將以下方法添加到類中:

func addOverlay() {
  let overlay = ParkMapOverlay(park: park)
  mapView.add(overlay)
}

此方法將MKOverlay添加到地圖視圖中。

如果用戶應選擇顯示地圖疊加層,則loadSelectedOptions()應調(diào)用addOverlay()。 使用以下代碼替換loadSelectedOptions()

func loadSelectedOptions() {
  mapView.removeAnnotations(mapView.annotations)
  mapView.removeOverlays(mapView.overlays)
  
  for option in selectedOptions {
    switch (option) {
    case .mapOverlay:
      addOverlay()
    default:
      break;
    }
  }
}

每當用戶關閉選項選擇視圖時,應用程序調(diào)用loadSelectedOptions(),然后確定所選選項,并調(diào)用適當?shù)姆椒ㄔ诘貓D視圖上呈現(xiàn)這些選擇。

loadSelectedOptions()還會刪除可能存在的任何annotationsoverlays,以便您不會最終出現(xiàn)重復的渲染。 這不一定有效,但它是從地圖中清除先前項目的簡單方法。

要實現(xiàn)代理方法,請將以下方法添加到文件底部的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在視圖中時,地圖視圖將上述方法作為委托調(diào)用。

在這里,您檢查疊加層是否屬于類型ParkMapOverlay。 如果是這樣,則加載疊加圖像,使用疊加圖像創(chuàng)建ParkMapOverlayView實例,并將此實例返回給調(diào)用者。

但是有一個小問題 - 那可疑的小overlay_park圖片來自哪里?

這是一個PNG文件,其目的是覆蓋公園邊界的地圖視圖。 overlay_park圖像(在image assets中找到)如下所示:

Build并運行,選擇Map Overlay選項,瞧! 在地圖上方繪制了公園覆蓋圖:

根據(jù)需要放大,縮小和移動 - 疊加視圖按照您的預期進行縮放和移動。

后記

本篇主要講述了一個疊加視圖相關的簡單示例,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容