上一篇談了整體的設計思路,這篇談一下具體的實現設計。因為我的項目里第一個接入的地圖源是高德地圖,這里的接口以高德地圖作為示范。
既然要接入多個地圖源,可以良好的支持地圖源切換,那么第一步就是隔離具體地圖源。隔離具體實現最常使用的方式就是使用接口隔離。UITableView 中常用的 UITableViewDataSource 也是類似的機制,使用接口隔離了具體的 dataSource 實現。
我們定義一個 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 類型。
下一步要做的是讓地圖源實現這個接口。
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 進行隔離。
接著補充一下控件的初始化方法:
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)
}
}
到這里我們就完成地圖源的隔離與封裝。