在封裝了地圖源以後,咱們開始實現最經常使用的功能,自定義 UI 展現。這裏我以繪製一個標註舉例。 自定義 UI 能夠用 CoreGraphic 繪製,也能夠用傳統的 UIKit 那套。我這裏自定義標註的樣式並不複雜,我使用傳統的 UIView 展現。固然有一些圖形的 UI 我也有用到 CoreGraphic。swift
由於個人自定義標註出了圖片外還有一些信息要展現(標註標題),所以我用一個數據結構來表示標註的信息。bash
public enum MeshAnnotationType {
case homePoint
}
public class MeshMapAnnotation {
public var type: MeshAnnotationType
init(type: MeshAnnotationType) {
self.type = type
}
}
複製代碼
由於是很簡單的標註樣式,因此 View 的實現也很簡單:數據結構
class MeshAnnotaionView: UIView {
private(set) var annotion: MeshMapAnnotation
private(set) var imageView = UIImageView(image: nil)
init(annotion: MeshMapAnnotation) {
self.annotion = annotion
super.init(frame: CGRect.zero)
addSubview(imageView)
setupUI()
}
private func setupUI() {
switch type {
case .homePoint:
imageView.image = Asset.Map.iconMapHomepoint.image
imageView.frame = CGRect(x: 0, y: 0, width: 32, height: 32)
bounds = imageView.frame
default:
break
}
}
}
複製代碼
這裏的樣式就是簡單的一個 icon。標註的樣式這裏就不展開講了(不是重點),總之就是本身實現了一個 View。 下一步咱們定義一個 CustomMapOverlayView 專門用來管理繪製自定義的須要展現的 UI。ide
class CustomMapOverlayView: UIView {
private var homePointAnnotationView: MeshAnnotaionView?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
isUserInteractionEnabled = false
}
func updateHomePoint(_ point: CGPoint?) {
if let point = point {
if homePointAnnotationView == nil {
let annotation = MeshMapAnnotation(type: .homePoint)
homePointAnnotationView = MeshAnnotaionView(annotion: annotation)
addSubview(homePointAnnotationView!)
}
homePointAnnotationView?.center = point
} else {
homePointAnnotationView?.removeFromSuperview()
}
}
}
複製代碼
CustomMapOverlayView 的一個細節要把 isUserInteractionEnabled 設爲 false,由於這層覆蓋在地圖源上層,若是也響應交互事件,那麼用戶就沒法拖動、縮放地圖了。 至於自定義標註在這個 View 裏怎麼管理,就看各自的業務場景。由於我這裏的地圖標註就幾個類型,直接定義成了可選的屬性。若是要給上層更大的靈活性,也能夠用字典存儲。post
在目前這個結構裏,咱們的自定義標註是能夠脫離地圖單獨測試的。咱們能夠在測試項目中,直接初始化 CustomMapOverlayView,調用 updateHomePoint 就能夠渲染出 homePointView。自定義 UI 的元素就能夠良好的支持單元測試。這也是在設計的時候要考慮到一點,每個單元儘可能內聚。和外部經過數據鏈接,自身的邏輯能夠獨立的運行。這樣最後總體的結構就會是各個小單元鏈接起來,而不是一堆單元直接焊死在一塊兒。單元測試
接下來咱們把 CustomMapOverlayView 集成到地圖控件中:測試
public class MeshMapView: UIView {
let customOverlayView: CustomMapOverlayView
public init() {
customOverlayView = CustomMapOverlayView(frame: CGRect.zero)
super.init(frame: CGRect.zero)
addVendorMapView()
addSubview(customOverlayView)
customOverlayView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
}
複製代碼
這裏須要稍微注意一下圖層的順序,由於自定義 UI 層要在地圖源上方,所以須要先添加地圖,再添加自定義 View。ui
集成以後就能夠暴露接口給外部調用:spa
public class MeshMapView: UIView {
public var homePoint: CLLocationCoordinate2D? {
didSet {
updateHomePoint(homePoint)
}
}
private func updateHomePoint(_ coordinate: CLLocationCoordinate2D?) {
let correspondingPoint = convertCoordinateToCustomOverlayView(coordinate: coordinate)
customOverlayView.updateHomePoint(correspondingPoint)
}
private func convertCoordinateToCustomOverlayView(coordinate: CLLocationCoordinate2D?) -> CGPoint? {
guard let coordinate = coordinate else { return nil }
let standardCoordindate = MeshMapView.convertCoordinateToGCJIfNeeded(coordinate: coordinate)
guard let point = map?.convert(coordinate: standardCoordindate, toPointTo: customOverlayView) else { return nil }
if point.x.isNaN || point.y.isNaN { //若是轉換時地圖尚未加載完會返回無效的點
return nil
}
return point
}
}
複製代碼
國內地圖使用的座標都是 GCJ,可是國際上不少地方存的都是 GPS 座標,所以這裏在轉換座標的時候調用了一個接口將 WGS84 轉成 GCJ,固然這裏的偏差確定是有的。 上面的代碼重點是,咱們須要保存標註的地理座標,添加到 customOverlayView 以前須要將地理座標轉換爲平面座標。轉換完成以後就能夠調用 customOverlayView.updateHomePoint 了。設計
還有一個細節是地理座標到平面座標的轉換,有時地圖沒加載完會轉換失敗。由於 CGPoint 是值類型,有的地圖 SDK 轉換失敗會轉出值爲 NaN。轉換後還須要判斷一下 x 和 y 的值是不是有效的。
完成上面的代碼後,調用 updateHomePoint 後能夠展現標註的位置了。可是目前這個實現還有一個問題,當用戶移動地圖的時候,標註在視圖的位置沒有跟着一下變更。正確的反應應該是地圖位置變了,標註的位置也跟着一塊兒變化。很天然的,咱們須要監聽地圖區域變化通知,接着更新標註的位置。
首先咱們聲明一個類當作地圖源的代理對象:
protocol VendorMapDelegate: class {
func mapViewDidChange()
func mapInitComplete()
}
class VendorMapDelegateProxy: NSObject, MAMapViewDelegate {
weak var delegate: VendorMapDelegate?
init(vendorMapDelegate: VendorMapDelegate) {
self.delegate = vendorMapDelegate
super.init()
}
func mapViewRegionChanged(_ mapView: MAMapView!) {
delegate?.mapViewDidChange()
}
func mapInitComplete(_ mapView: MAMapView!) {
delegate?.mapInitComplete()
}
}
複製代碼
聲明瞭一個通用的接口 VendorMapDelegate 來表示地圖源的通知事件。由於每一個時刻只會有一個地圖源存在,所以 VendorMapDelegateProxy 也只會有一個實例和 MeshMapView 關聯。
mapInitComplete 方法是高德特有的,不一樣的地圖 SDK 有不一樣的方式表示本身加載完成,有的是 finishLoading,高德則是 mapInitComplete。地圖加載完成的事件外界也會關心,所以也聲明瞭這個方法。
接着咱們把代理對象集成到 MeshMapView 中:
public class MeshMapView: UIView {
public var homePoint: CLLocationCoordinate2D? {
didSet {
updateHomePoint(homePoint)
}
}
private lazy var mapDelegateProxy: VendorMapDelegateProxy = {
return VendorMapDelegateProxy(vendorMapDelegate: self)
}()
private func addVendorMapView() {
switch MeshMapView.currentMapVendor {
case .gaode:
let gaodeMap = MAMapView(frame: CGRect.zero)
gaodeMap.delegate = mapDelegateProxy
addSubview(gaodeMap)
gaodeMap.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.gaodeMap = gaodeMap
case .baidu:
// 。。。
}
}
func refreshCustomOverlay() {
updateHomePoint(homePoint)
}
}
extension MeshMapView: VendorMapDelegate {
func mapViewDidChange() {
refreshCustomOverlay()
}
func mapInitComplete() {
//。。。
}
}
複製代碼
集成這段代碼後能夠看出爲何以前須要保存 homePoint 的地理座標了:由於地圖區域變化後須要從新渲染標註,須要元數據從新映射平面座標,好更新位置。
這個模塊的設計要點是 CustomMapOverlayView 的職責必定要劃分清楚,它只接受平面座標更新位置。這樣 CustomMapOverlayView 能夠和業務解耦,只是特供了標註的繪製能力。而地圖控件須要管理座標轉換,地圖區域變更後的從新渲染的時機。