上一篇談了總體的設計思路,這篇談一下具體的實現設計。由於個人項目裏第一個接入的地圖源是高德地圖,這裏的接口以高德地圖做爲示範。swift
既然要接入多個地圖源,能夠良好的支持地圖源切換,那麼第一步就是隔離具體地圖源。隔離具體實現最常使用的方式就是使用接口隔離。UITableView 中經常使用的 UITableViewDataSource 也是相似的機制,使用接口隔離了具體的 dataSource 實現。post
咱們定義一個 protocol 來聲明地圖源應該提供的能力:優化
public protocol VendorMapView: class {
/// 實際座標轉換到指定 View 上座標
func convert(coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint
/// 轉換 View 上的點爲實際座標
func convert(point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D
func setCenter(coordinate: CLLocationCoordinate2D)
}
複製代碼
我簡化了代碼,這裏只聲明瞭核心的座標轉換方法和做爲演示的設置地圖中心座標的方法。聲明實現的對象須要是類是由於咱們明確的知道實現這個接口的對象是具體的地圖源,是 UIView 類型。ui
下一步要作的是讓地圖源實現這個接口。spa
import MAMapKit
extension MAMapView: VendorMapView {
public func convert(coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint {
return convert(coordinate, toPointTo: view)
}
public func convert(point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D {
return convert(point, toCoordinateFrom: view)
}
public func setCenter(coordinate: CLLocationCoordinate2D) {
setCenter(coordinate, animated: true)
}
}
複製代碼
到這裏咱們已經隔離了具體的地圖源了。假設咱們自定義地圖名爲 MeshMapView,如今在咱們自定義地圖中聲明地圖代理:設計
public class MeshMapView: UIView {
public static var currentMapVendor = MapVendor.gaode
var gaodeMap: MAMapView?
var baiduMap: BMKMapView?
var map: VendorMapView? {
switch MeshMapView.currentMapVendor {
case .gaode:
return gaodeMap
case .baidu:
return baiduMap
}
}
}
extension MeshMapView {
/// 地圖提供商
public enum MapVendor: CaseIterable {
case gaode, baidu
var descrption: String {
switch self {
case .gaode:
return "高德"
case .baidu:
return "百度"
}
}
}
}
複製代碼
由於地圖控件是針對業務封裝的,可能有不少業務相關的枚舉類型,所以在單獨的 extension 中聲明地圖控件的相關枚舉。咱們須要知道當前的地圖源是哪個供應商,所以使用 MapVendor 列出全部的地圖供應商。代理
在個人業務場景裏,若是在某個頁面選擇了某個地圖源,那麼以後全部的地圖控件都使用這個地圖源。從這個需求出發,所以當前選擇的地圖源是一個全局的設置,所以聲明爲靜態屬性。 具體地圖源的選擇分發咱們用 VendorMapView 類型的 map 進行隔離。code
接着補充一下控件的初始化方法:對象
import SnapKit
public class MeshMapView: UIView {
public init() {
super.init(frame: CGRect.zero)
addVendorMapView()
}
private func addVendorMapView() {
switch MeshMapView.currentMapVendor {
case .gaode:
let gaodeMap = MAMapView(frame: CGRect.zero)
gaodeMap.mapType = .satellite
gaodeMap.zoomLevel = 16.5
addSubview(gaodeMap)
gaodeMap.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.gaodeMap = gaodeMap
case .baidu:
// 。。。
}
}
}
複製代碼
到這裏的代碼實現了經過 currentMapVendor 屬性能夠配置地圖控件的地圖源。若是要增長一個地圖源,只須要讓新地圖源實現 VendorMapView,MapVendor 枚舉增長一個類型,最後在地圖控件中增長實例的初始化方法。這個設計對地圖源的新增開放,不須要修改原有的代碼邏輯,經過新增長代碼就能夠實現,容易維護。接口
不過上面的 addVendorMapView 方法還有優化的空間。每一個地圖的初始化配置的邏輯是具體實現,嚴格的說和 MeshMapView 並不直接相關,MeshMapView 不關心具體地圖供應商的配置。所以能夠把地圖源初始化配置代碼移到地圖源自身擴展中:
public protocol VendorMapView: class {
func initialConfig()
}
extension MAMapView: VendorMapView {
func initialConfig() {
mapType = .satellite
zoomLevel = 16.5
}
}
複製代碼
可是初始化配置的代碼寫在一個地方也是能夠接受的。好處是若是一個通用的配置,好比地圖的默認 zoomLevel 要改成 10,若是初始化代碼寫在一塊兒只在一個地方改就能夠了,不用去四處找。這裏個人想法是雖然幾個地圖源初始化配置寫在一塊兒方法的長度可能會有三四十行,可是初始化代碼邏輯複雜度很低,寫在一個方法裏也是能夠接受的。看開發者我的喜愛了。
最後一步咱們要暴露自定義地圖控件的地圖相關方法。由於這類方法只是封裝了一層,最後是直接調用到具體地圖源,不是業務相關的,所以建議單獨寫在一個 extension 裏:
extension MeshMapView {
public func setCenter(coordinate: CLLocationCoordinate2D) {
map?.setCenter(coordinate: standardCoordinate)
}
}
複製代碼
到這裏咱們就完成地圖源的隔離與封裝。