版本記錄
| 版本號 | 時間 |
|---|---|
| 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)建自己的疊加視圖之前,您需要了解兩個關鍵類:MKOverlay和MKOverlayRenderer。
MKOverlay告訴MapKit你想要繪制疊加層的位置。使用該類有三個步驟:
- 1) 創(chuàng)建自己的實現(xiàn)MKOverlay protocol協(xié)議的自定義類,該協(xié)議具有兩個必需屬性:
coordinate和boundingMapRect。這些屬性定義了疊加層在地圖上的位置以及疊加層的大小。 - 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在給定特定的MKMapRect,MKZoomScale和圖形上下文的CGContext時應如何呈現(xiàn)此視圖,以便以適當?shù)谋壤龑B加圖像繪制到上下文中。
Core Graphics繪圖的詳細信息遠遠超出了本教程的范圍。 但是,您可以看到上面的代碼使用傳遞的MKMapRect來獲取CGRect,以便確定在提供的上下文中繪制UIImage的CGImage的位置。
現(xiàn)在您已同時擁有MKOverlay和MKOverlayRenderer,您可以將它們添加到地圖視圖中。
在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()還會刪除可能存在的任何annotations和overlays,以便您不會最終出現(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ù)需要放大,縮小和移動 - 疊加視圖按照您的預期進行縮放和移動。
后記
本篇主要講述了一個疊加視圖相關的簡單示例,感興趣的給個贊或者關注~~~
