上一篇談了整體的設(shè)計(jì)思路执虹,這篇談一下具體的實(shí)現(xiàn)設(shè)計(jì)拓挥。因?yàn)槲业捻?xiàng)目里第一個(gè)接入的地圖源是高德地圖,這里的接口以高德地圖作為示范袋励。
既然要接入多個(gè)地圖源侥啤,可以良好的支持地圖源切換,那么第一步就是隔離具體地圖源茬故。隔離具體實(shí)現(xiàn)最常使用的方式就是使用接口隔離愿棋。UITableView 中常用的 UITableViewDataSource 也是類似的機(jī)制,使用接口隔離了具體的 dataSource 實(shí)現(xiàn)均牢。
我們定義一個(gè) protocol 來(lái)聲明地圖源應(yīng)該提供的能力:
public protocol VendorMapView: class {
/// 實(shí)際坐標(biāo)轉(zhuǎn)換到指定 View 上坐標(biāo)
func convert(coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint
/// 轉(zhuǎn)換 View 上的點(diǎn)為實(shí)際坐標(biāo)
func convert(point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D
func setCenter(coordinate: CLLocationCoordinate2D)
}
我簡(jiǎn)化了代碼,這里只聲明了核心的坐標(biāo)轉(zhuǎn)換方法和作為演示的設(shè)置地圖中心坐標(biāo)的方法才睹。聲明實(shí)現(xiàn)的對(duì)象需要是類是因?yàn)槲覀兠鞔_的知道實(shí)現(xiàn)這個(gè)接口的對(duì)象是具體的地圖源徘跪,是 UIView 類型。
下一步要做的是讓地圖源實(shí)現(xiàn)這個(gè)接口琅攘。
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)
}
}
到這里我們已經(jīng)隔離了具體的地圖源了垮庐。假設(shè)我們自定義地圖名為 MeshMapView,現(xiàn)在在我們自定義地圖中聲明地圖代理:
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 "百度"
}
}
}
}
因?yàn)榈貓D控件是針對(duì)業(yè)務(wù)封裝的坞琴,可能有很多業(yè)務(wù)相關(guān)的枚舉類型哨查,因此在單獨(dú)的 extension 中聲明地圖控件的相關(guān)枚舉。我們需要知道當(dāng)前的地圖源是哪一個(gè)供應(yīng)商剧辐,因此使用 MapVendor 列出所有的地圖供應(yīng)商寒亥。
在我的業(yè)務(wù)場(chǎng)景里,如果在某個(gè)頁(yè)面選擇了某個(gè)地圖源荧关,那么之后所有的地圖控件都使用這個(gè)地圖源溉奕。從這個(gè)需求出發(fā),因此當(dāng)前選擇的地圖源是一個(gè)全局的設(shè)置忍啤,因此聲明為靜態(tài)屬性加勤。
具體地圖源的選擇分發(fā)我們用 VendorMapView 類型的 map 進(jìn)行隔離。
接著補(bǔ)充一下控件的初始化方法:
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:
// 。鳄梅。叠国。
}
}
}
到這里的代碼實(shí)現(xiàn)了通過(guò) currentMapVendor 屬性可以配置地圖控件的地圖源。如果要增加一個(gè)地圖源戴尸,只需要讓新地圖源實(shí)現(xiàn) VendorMapView粟焊,MapVendor 枚舉增加一個(gè)類型,最后在地圖控件中增加實(shí)例的初始化方法校赤。這個(gè)設(shè)計(jì)對(duì)地圖源的新增開(kāi)放吆玖,不需要修改原有的代碼邏輯,通過(guò)新增加代碼就可以實(shí)現(xiàn)马篮,容易維護(hù)沾乘。
不過(guò)上面的 addVendorMapView 方法還有優(yōu)化的空間。每個(gè)地圖的初始化配置的邏輯是具體實(shí)現(xiàn)浑测,嚴(yán)格的說(shuō)和 MeshMapView 并不直接相關(guān)翅阵,MeshMapView 不關(guān)心具體地圖供應(yīng)商的配置。因此可以把地圖源初始化配置代碼移到地圖源自身擴(kuò)展中:
public protocol VendorMapView: class {
func initialConfig()
}
extension MAMapView: VendorMapView {
func initialConfig() {
mapType = .satellite
zoomLevel = 16.5
}
}
但是初始化配置的代碼寫在一個(gè)地方也是可以接受的迁央。好處是如果一個(gè)通用的配置掷匠,比如地圖的默認(rèn) zoomLevel 要改為 10,如果初始化代碼寫在一起只在一個(gè)地方改就可以了岖圈,不用去四處找讹语。這里我的想法是雖然幾個(gè)地圖源初始化配置寫在一起方法的長(zhǎng)度可能會(huì)有三四十行,但是初始化代碼邏輯復(fù)雜度很低蜂科,寫在一個(gè)方法里也是可以接受的顽决。看開(kāi)發(fā)者個(gè)人喜好了导匣。
最后一步我們要暴露自定義地圖控件的地圖相關(guān)方法才菠。因?yàn)檫@類方法只是封裝了一層,最后是直接調(diào)用到具體地圖源贡定,不是業(yè)務(wù)相關(guān)的赋访,因此建議單獨(dú)寫在一個(gè) extension 里:
extension MeshMapView {
public func setCenter(coordinate: CLLocationCoordinate2D) {
map?.setCenter(coordinate: standardCoordinate)
}
}
到這里我們就完成地圖源的隔離與封裝。