上一篇谈了整体的设计思路,这篇谈一下具体的实现设计。因为我的项目里第一个接入的地图源是高德地图,这里的接口以高德地图作为示范。
既然要接入多个地图源,可以良好的支持地图源切换,那么第一步就是隔离具体地图源。隔离具体实现最常使用的方式就是使用接口隔离。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)
}
}
到这里我们就完成地图源的隔离与封装。