翻譯自:https://www.raywenderlich.com/158175/advanced-mapkit-tutorial-custom-tiles
地圖在現(xiàn)代應用中無處不在。地圖可以提供附近興趣點(POI)的位置, 幫助用戶導覽一個商業(yè)區(qū)或者公園, 尋找附近的朋友, 旅游足跡追蹤或者為增強現(xiàn)實游戲提供上下文。
遺憾的是, 這意味著嵌入在應用中的大多數(shù)地圖看起來一樣。

本教程介紹了如何添加手繪地圖, 而不是編程式生成的地圖, 如同口袋妖怪GO(PokemonGo)中地圖。
手繪地圖是一項重要的工作??紤]到地球的大小, 它僅僅適用于定義明確,小范圍地理區(qū)域。 如果在你腦海中已經(jīng)有了良好定義的區(qū)域地圖, 那么自定義地圖會為你的 app 贏得大量贊嘆。
此處下載工程模板。
MapQuest是一款有趣的冒險游戲,我們就以它開始吧。英雄在中央公園,即現(xiàn)實生活中的紐約城(NYC)里到處亂跑, 但是從冒險開始, 大怪物和收集財寶都是在實境中完成。它的設計如此可愛和童稚,讓玩家感覺很舒服并不感到乏味。
游戲中有幾個興趣點(POI)。這些定義位置的興趣點可以讓玩家與游戲進行交互。 這些可以是任務,怪物,商店或者其他游戲元素。 進入 POI 周圍 10 米范圍內即認為是相遇。為了本教程, 實際的游戲相對于地圖渲染是次要的。
工程中包含兩個重要文件:
MapViewController.swift: 管理地圖視圖,處理用戶交互邏輯及狀態(tài)變化。
Game.swift: 包含了游戲邏輯以及管理一些游戲對象的坐標。
游戲的主要視圖是一個MKMapView。 在所有的縮放層級 MapKit 使用 瓦片來填充視圖并且提供地理特征,道路等的信息。
地圖視圖既可以展示傳統(tǒng)的公路地圖也可以展示衛(wèi)星地圖。這在城市導航中非常有用,但是想象你在中世紀世界的冒險而言是無用的。然而, MapKit 允許你提供自己的地圖圖片來自定義地圖展示效果及信息展示。
地圖視圖是由許多瓦片組成的,當你在視圖周圍平移時,它們會被動態(tài)加載。 瓦片是256像素256像素,被嵌入到一個網(wǎng)格中,對應一個墨卡托投影地圖。
編譯并運行 app, 查看運行中的地圖。

哇! 多么美麗的商業(yè)區(qū)。 游戲的主要界面是定位, 這意味著在未到達中央公園時你什么也看不到也不能做任何操作。
與其他教程不同的是, MapQuest 一開始就是一款功能性 app! 但是, 如果你不住在紐約市,? 那就有點小小的遺憾。幸運的是, XCode 提供至少兩種模擬位置的方法。
當 app 在 iPhone 模擬器處于運行狀態(tài)時,設置用戶的位置。
定位到Debug\Location\Custom Location….
設置維度值為40.767769,經(jīng)度值為-73.971870。這將激活藍色用戶位置點并將地圖定位到中央公園動物園。這里住著一只小妖精; 你將被迫進入戰(zhàn)斗然后收集財寶。

在打敗無助的小妖精后,你將會被定位到動物園內 (注意藍色點)。

對于許多基于位置的 app ,? 靜態(tài)位置測試是非常有用的。 然而,作為冒險的一部分這個游戲需要訪問多個地點。模擬器可以為跑步,騎行和駕駛更改位置。這些預置的行程只適用于庫比蒂諾(蘋果電腦的全球總公司所在地,位于美國舊金山), 但是 MapQuest 只會在紐約相遇。
像這樣的場合需要使用 GPX(GPS 交換格式) 文件來模擬位置。這個文件定義了許多導航點,模擬器將在它們之間生成一條路徑。
創(chuàng)建此文件不在本教程范圍之內, 但是示例工程已經(jīng)自帶了一個GPX測試文件供您使用。
在 Xcode 中通過選擇Product\Scheme\Edit Scheme….? 打開 scheme editor
在左面板中選擇Run, 接著選擇 右面板中的Options。 在Core Location區(qū)域, 點擊并選中Allow Location Simulation。在Default Location下拉控件中選擇Game Test。

這意味著 app 將會在Game Test.gpx文件中定義的 導航點之間移動。
編譯并運行。

模擬器將會從第五大道地鐵移動到中央公園動物園,在那兒你將同小妖精進行戰(zhàn)斗。 之后, 到達你最喜歡的水果公司旗艦店并在那里購買升級后的劍, 一次循環(huán)完成, 冒險會重新開始。
OpenStreetMap是一個社區(qū)支持的開放地圖數(shù)據(jù)庫。 這些數(shù)據(jù)可以用來生成被蘋果地圖使用的同樣類型的地圖瓦片。OpenStreetMap 社區(qū)不僅僅提供基本公路地圖,還包括專業(yè)地形地圖, 單車及藝術渲染。
注意:Open Street Map 瓦片 使用協(xié)議對數(shù)據(jù)的使用,歸屬及 API 訪問 有嚴格的要求。 對于教程來說比較友好, 但在生產環(huán)境的應用中使用 瓦片之前請檢查合規(guī)政策。
使用 一個MKTileOverlay替換地圖瓦片并在默認的蘋果地圖上顯示新瓦片。
打開MapViewController.Swift, 使用如下代碼替換setupTileRenderer():
func?setupTileRenderer()?{
?
//?1
?
let?template?=?"https://tile.openstreetmap.org/{z}/{x}/{y}.png"
?
//?2
?
let?overlay?=?MKTileOverlay(urlTemplate:?template)
?
//?3
?
overlay.canReplaceMapContent?=?true
?
//?4
?
mapView.add(overlay,?level:?.aboveLabels)
?
//5
?
tileRenderer?=?MKTileOverlayRenderer(tileOverlay:?overlay)
?
}
默認情況下,MKTileOverlay支持通過瓦片路徑的 URL 鏈接來加載瓦片。
這是 Open Street Map 提供的用于下載地圖瓦片的 API。{x},{y}和{z}在運行時會被各個瓦片的坐標來替換。 z-坐標, 或者縮放級別是由用戶在地圖中縮放了多少來指定的。x和y是給定所示地球部分的瓦片的索引。對于每個支持的縮放級別,每個瓦片都需要設置 x 和 y。
創(chuàng)建一個浮層.
設置瓦片為不透明并替換默認地圖瓦片。
將浮層添加到mapView。 自定義瓦片可以在公路或標簽(像 道路和地名)的上方。Open street map 瓦片為預先標記, 這些瓦片應該在蘋果標簽的上方。
創(chuàng)建一個瓦片渲染器用來繪制瓦片。
在瓦片展示之前, 必須創(chuàng)建瓦片渲染器用于繪制瓦片。
在viewDidLoad()方法末尾 添加如下代碼:
mapView.delegate?=?self
設置MapViewController為mapView的代理。
接著, 在 MapView 代理擴展中,添加如下方法:
func?mapView(_?mapView:?MKMapView,?rendererFor?overlay:?MKOverlay)?->?MKOverlayRenderer?{
?
return?tileRenderer
?
}
浮層渲染器 通知 地圖視圖 如何去繪制一個浮層。 瓦片渲染器(MKTileOverlayRenderer) 是浮層渲染器(MKOverlayRenderer)的子類,用于加載和繪制地圖瓦片。
就是這些! 編譯并運行觀察替換標準蘋果地圖后的 Open Street Map。

此刻, 你會看到 蘋果地圖與開源地圖的不同!
瓦片浮層的魔力是將瓦片路徑轉換到圖片資源。 瓦片的路徑由他們的坐標:x,y和z 來表示。? x 和 y 相當于地圖表面的索引, 0,0 表示左上的瓦片。? z-坐標 代表縮放層級并決定了當前層級的地圖上有多少個瓦片組成。
在縮放層級 0, 世界地圖由一個 1*1 的網(wǎng)格表示, 需要一個瓦片:

在縮放層級 1, 世界地圖被劃分為 2*2 的網(wǎng)格。這需要4個瓦片:

在層級 2, 網(wǎng)格的行和列再次翻倍, 需要 16 個瓦片:

按照這用模式繼續(xù)下去,每個縮放級別的細節(jié)水平和瓷磚數(shù)量都要翻一番,每個縮放層級需要 22*z個瓦片, 一直下去直到縮放層級 19 需要 274,877,906,944 個瓦片!
因為地圖視圖是按照用戶的位置來設置的, 默認的縮放層級設置為 16,? 這個層級可以更好的展示用戶位置. 縮放層級 16 的整個地圖需要 4,294,967,296 個瓦片! 這需要花費一生的時間來手繪這些瓦片。
擁有一個更小的區(qū)域,如商業(yè)區(qū)或者公園使得手繪自定義瓦片成為可能。 對于更大區(qū)域的位置,可以使用程序處理資源數(shù)據(jù)來生成瓦片。
因為本游戲中的瓦片是預渲染的并且包含在資源中, 你可以方便的加載它們。不幸的是, 一個通用的 URL 模板是不夠的, 如果渲染器請求數(shù)十億個瓦片中并不包含在應用資源中的任一瓦片,最好優(yōu)雅的失敗。
做到這一點, 你需要自定義一個MKTileOverlay的子類。 打開AdventureMapOverlay.swift并添加如下代碼:
lass?AdventureMapOverlay:?MKTileOverlay?{
?
override?func?url(forTilePath?path:?MKTileOverlayPath)?->?URL?{
?
let?tileUrl?=?"https://tile.openstreetmap.org/(path.z)/(path.x)/(path.y).png"
?
return?URL(string:?tileUrl)!
?
}
?
}
這樣就創(chuàng)建了一個子類, 并使用具有特定URL生成器的模板URL替換基本類。
保持 Open Street Map 瓦片可用,并測試自定義浮層。
打開MapViewController.swift,使用如下代碼替換setupTileRenderer():
func?setupTileRenderer()?{
?
let?overlay?=?AdventureMapOverlay()
?
overlay.canReplaceMapContent?=?true
?
mapView.add(overlay,?level:?.aboveLabels)
?
tileRenderer?=?MKTileOverlayRenderer(tileOverlay:?overlay)
?
}
此處替換為自定義的子類。
再次編譯并運行。 如果一切順利, 展示的游戲與之前的一樣。 哇!

現(xiàn)在到了有趣的部分。打開AdventureMapOverlay.swift, 使用如下代碼替換url(forTilePath:):
override?func?url(forTilePath?path:?MKTileOverlayPath)?->?URL?{
?
//?1
?
let?tilePath?=?Bundle.main.url(
?
forResource:?"(path.y)",
?
withExtension:?"png",
?
subdirectory:?"tiles/(path.z)/(path.x)",
?
localization:?nil)
?
guard?let?tile?=?tilePath?else?{
?
//?2
?
return?Bundle.main.url(
?
forResource:?"parchment",
?
withExtension:?"png",
?
subdirectory:?"tiles",
?
localization:?nil)!
?
}
?
return?tile
?
}
本段代碼為游戲加載自定義瓦片。
首先, 使用已知的命名方案在資源文件中找到一個匹配的文件。
如果某個瓦片沒有提供, 使用一個羊皮紙樣式的圖片代替,這能使人幻想起世紀的感覺。 這也省卻了需要每瓦的路徑提供一個唯一的資源。
再次編譯并運行。 現(xiàn)在展示自定義地圖。

試著縮放地圖,查看不同層級的地圖細節(jié)。

不要縮放的太遠, 否則你將會看不到整個地圖。

幸運的是,這很好解決。打開MapViewController.swift,在setupTileRenderer() 方法末尾添加如下代碼:
overlay.minimumZ?=?13
overlay.maximumZ?=?16
這會通知mapView瓦片只能在這些縮放層級中提供。 當縮放層級超過了限定范圍,瓦片的圖片由 app 提供。 沒有提供更多的細節(jié), 但至少現(xiàn)在顯示的圖片可以匹配.

下一部分是可選的, 因為它介紹了如何繪制定義的瓦片。 跳過更多的MapKit 技術,跳到 "美化地圖" 那部分。
本演示最困難的部分是創(chuàng)建大小合適的瓦片并把它們都排好。 要想繪制你自定義的瓦片,你需要一個數(shù)據(jù)源和圖片編輯器。
打開項目目錄同時查看MapQuest/tiles/14/4825/6156.png。這個瓦片圖片顯示了縮放層級為 14 時的中央公園地圖底部地圖圖片。app 包含許多這樣的小圖片用以組成紐約市的地圖,每個圖片是采用基本的技術和工具手繪完成的。

第一步計算出將要繪制的是哪張瓦片。你可以從Open Street Map下載資源數(shù)據(jù)并使用類似MapNik的工具將其生成瓦片圖片。不幸的是, 資源有 57GB ! 使用這個工具有點難度并且已經(jīng)超出本教程的范圍。
對于一個有界區(qū)域如中央公園, 有更簡單的解決方案。
打開AdventureMapOverlay.swift,在url(forTilePath:)中 添加如下代碼:
print("requested?tile\tz:(path.z)\tx:(path.x)\ty:(path.y)")
編譯并運行。 當你縮放并且平鋪到地圖上, 控制臺會打印出 瓦片的路徑。

接著 獲取資源瓦片并進行自定義。你可以重用之前的 URL 方案 來獲取 open street map 瓦片。
以下 terminal 命令將會下載文件并保存到本地。 你可以修改 URL, 使用指定的地圖路徑來替換 x, y 和 z。
curl --create-dirs -o z/x/y.pnghttps://tile.openstreetmap.org/z/x/y.png
使用中央公園南部地區(qū), 試試:
curl --create-dirs -o14/4825/6156.pnghttps://tile.openstreetmap.org/14/4825/6156.png

zoom-level/x-coordinate/y-coordinate這個目錄結構 使得后續(xù)查找和使用瓦片變得非常容易。
下一步是使用定制的基礎圖像作為起點。在你喜歡的圖片編輯器中打開瓦片圖片。例如, 下圖為使用 Pixelmator 打開文件的界面:

現(xiàn)在你可以使用刷子和鉛筆工具繪制道路,路徑及有趣的特征。

如果你的工具支持圖層, 在不同的圖層上繪制不同的特征將允許您調整它們以提供最佳的外觀。使用圖層使繪圖更加容易些,因為您可以掩蓋其他特征下面的凌亂線條。


現(xiàn)在在設置的所有瓦片上重復這樣的操作。 正如你所看到的, 這將會花費不少時間。
你可以使這個過程更容易一些:
首先將整個層的所有瓦片組合在一起。
繪制自定義地圖。
再把地圖分割為瓦片。

創(chuàng)建完新瓦片后, 將它們放回到項目的tiles/zoom-level/x-coordinate/y-coordinate目錄結構下. 這樣可以將資源分組管理并便于訪問。
這意味著你能很方便的訪問到瓦片, 在url(forTilePath:) 方法中添加如下代碼.
let?tilePath?=?Bundle.main.url(
?
forResource:?"(path.y)",
?
withExtension:?"png",
?
subdirectory:?"tiles/(path.z)/(path.x)",
?
localization:?nil)
這一階段完成?,F(xiàn)在你可以開始繪制一些漂亮的地圖!
地圖看起來不錯,符合游戲的審美。但是還有許多需要美化!
用藍點表示英雄不太美觀, 但是你可以使用自定義控件來代替當前的位置標注。
打開MapViewController.swift,在 MapView 代理擴展中添加如下方法 :
func?mapView(_?mapView:?MKMapView,?viewFor?annotation:?MKAnnotation)?->?MKAnnotationView??{
?
switch?annotation?{
?
//?1
?
case?let?user?as?MKUserLocation:
?
//?2
?
let?view?=?mapView.dequeueReusableAnnotationView(withIdentifier:?"user")
?
???MKAnnotationView(annotation:?user,?reuseIdentifier:?"user")
?
//?3
?
view.image?=?#imageLiteral(resourceName:?"user")
?
return?view
?
default:
?
return?nil
?
}
?
}
這段代碼自定義了用戶標注視圖。
使用 MKUserLocation 標注用戶位置。
MapViews 維護一個標注視圖重用緩沖池,便于提高性能。如果重用池中沒有標注視圖,則會創(chuàng)建一個。
一個標準的MKAnnotationView相當靈活,但是此處僅僅展示一張代表冒險家的圖片。
編譯并運行。 將會有一個小的簡筆人物畫替代藍點在屏幕上徘徊。

MKMapView同時允許你去標注你感興趣的位置。 MapQuest 連同紐約地鐵,將地鐵系統(tǒng)視為一個巨大的隧道網(wǎng)絡。
在地鐵站附近的地圖上添加一些標記點。 打開MapViewController.swift, 在viewDidLoad()方法的末尾添加如下代碼:
mapView.addAnnotations(Game.shared.warps)
編譯并運行, 現(xiàn)在一些精選的地鐵站使用大頭針來表示。

如同 用戶位置的藍點, 這些標準大頭針和游戲美學并不匹配 可以自定義標注。
在mapView(_:viewFor:)方法的 switch 語句 的 default case 之上添加如下的 case :
case?let?warp?as?WarpZone:
?
let?view?=?mapView.dequeueReusableAnnotationView(withIdentifier:?WarpAnnotationView.identifier)
?
???WarpAnnotationView(annotation:?warp,?reuseIdentifier:?WarpAnnotationView.identifier)
?
view.annotation?=?warp
?
return?view
再次編譯并運行。 自定義標注視圖將使用模板圖像,并為特定的地鐵線路著色。

MapKit 提供了許多方法用于美化游戲的地圖。接著
MapKit 提供了許多方法用于美化游戲地圖。 接下來, 使用一個MKPolygonRenderer在 水庫上繪制一個漸變的閃光效果。
使用如下代碼替換setupLakeOverlay():
func?setupLakeOverlay()?{
?
//?1
?
let?lake?=?MKPolygon(coordinates:?&Game.shared.reservoir,?count:?Game.shared.reservoir.count)
?
mapView.add(lake)
?
//?2
?
shimmerRenderer?=?ShimmerRenderer(overlay:?lake)
?
shimmerRenderer.fillColor?=?#colorLiteral(red:?0.2431372549,?green:?0.5803921569,?blue:?0.9764705882,?alpha:?1)
?
//?3
?
Timer.scheduledTimer(withTimeInterval:?0.1,?repeats:?true)?{?[weak?self]?_?in
?
self?.shimmerRenderer.updateLocations()
?
self?.shimmerRenderer.setNeedsDisplay()
?
}
?
}
這樣會創(chuàng)建一個新浮層:
創(chuàng)建一個同水庫形狀一樣的MKPolygon標注。這些坐標是寫死到Game.swift文件中的。
創(chuàng)建一個自定義渲染器用于特定的效果。
因為浮層渲染器不會提供動畫效果, 因此創(chuàng)建一個每 100ms 更新一次的定時器來更新浮層。
在下來, 使用如下代碼替換mapView(_:rendererFor:):
func?setupLakeOverlay()?{
?
//?1
?
let?lake?=?MKPolygon(coordinates:?&Game.shared.reservoir,?count:?Game.shared.reservoir.count)
?
mapView.add(lake)
?
//?2
?
shimmerRenderer?=?ShimmerRenderer(overlay:?lake)
?
shimmerRenderer.fillColor?=?#colorLiteral(red:?0.2431372549,?green:?0.5803921569,?blue:?0.9764705882,?alpha:?1)
?
//?3
?
Timer.scheduledTimer(withTimeInterval:?0.1,?repeats:?true)?{?[weak?self]?_?in
?
self?.shimmerRenderer.updateLocations()
?
self?.shimmerRenderer.setNeedsDisplay()
?
}
?
}
這將會為兩個浮層中的每一個選擇正確的渲染器。
再次編譯并運行。 然后到水庫去看波光粼粼的水!


想要快速學習? 觀看我們的視頻課程以便節(jié)省時間
你可以下載本教程的完整項目。
創(chuàng)建手繪地圖瓦片非常耗時, 但是可以給使用 app 的玩家一種身臨其境的感覺。 除了創(chuàng)建資源,使用它們非常簡單。
除了基本的瓦片, Open Street Map 列出了特定瓦片提供服務清單如騎行和地形。 Open Street Map 也提供了供你使用的數(shù)據(jù),這些數(shù)據(jù)可以幫你以編程的方式設計你自己的瓦片。
如果你想自定義但有逼真的地圖展示, 而不需要手繪所有東西, 可以使用第三方的工具,如MapBox。它價格設置合理,你可以方便的定制地圖的外觀。
更多關于自定義浮層和標注的信息, 參考另一篇教程。
如果你有任何問題或評論關于本教程,請加入下面的討論!
Open Street Map 數(shù)據(jù)和圖片由 ? OpenStreetMap 提供。