上一篇谈了总体的设计思路,这篇谈一下具体的实现设计。由于个人项目里第一个接入的地图源是高德地图,这里的接口以高德地图做为示范。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)
}
}
复制代码
到这里咱们就完成地图源的隔离与封装。