有的時候地圖控件上還會有交互需求。好比在個人業務場景裏,須要編輯無人機巡航的區域。要編輯區域,區域的多邊形頂點就須要能夠拖動。swift
簡化一下需求,咱們如今來實現一下點拖動編輯的功能。首先咱們定義一下能夠移動的頂點 View:架構
class MapMoveableAnnotation: UIImageView {
init(type: MoveableAnnotationType) {
self.type = type
super.init(frame: CGRect.zero)
isUserInteractionEnabled = true
setupUI()
}
private func setupUI() {
bounds = CGRect(x: 0, y: 0, width: 44, height: 44)
contentMode = .center
image = Asset.Map.iconAreaEditPoint.image
highlightedImage = Asset.Map.iconAreaEditPointSelected.image
}
}
複製代碼
上面要注意的細節是須要設置 isUserInteractionEnabled 爲 true。ide
接着咱們定義一個 view 專門響應用戶的交互事件,由於要首先響應交互,這個 view 要在地圖控件的最頂層。post
class MapInteractionView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in subviews {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
}
複製代碼
由於 MapInteractionView 裏全部元素都會響應交互事件,所以直接從新實現了 hitTest 方法。若是交互的點在內部元素上響應事件,若是不在的話不響應,讓交互事件能夠傳到下層的地圖源響應。單元測試
接下來咱們在 MapInteractionView 中定義添加可拖動控制點的方法:測試
class MapInteractionView: UIView {
private var areaControlAnnotations: [MapMoveableAnnotation] = []
func renderAreaEdit(vertexs: [CGPoint]) {
if areaControlAnnotations.count != vertexs.count {
areaControlAnnotations.forEach { $0.removeFromSuperview() }
areaControlAnnotations.removeAll()
areaControlAnnotations = vertexs.map { _ in MapMoveableAnnotation() }
areaControlAnnotations.forEach {
addSubview($0)
}
}
for (index, point) in vertexs.enumerated() {
areaControlAnnotations[index].center = point
areaControlAnnotations[index].tag = index
}
}
}
複製代碼
接着咱們給 MapInteractionView 添加手勢響應:ui
protocol MapInteractionDelegate: class {
func areaVertexMoved(index: Int, point: CGPoint, isFinish: Bool)
func areaFinishChanging()
}
class MapInteractionView: UIView {
weak var delegate: MapInteractionDelegate?
private var panGesture: UIPanGestureRecognizer?
override init(frame: CGRect) {
super.init(frame: frame)
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
addGestureRecognizer(panGesture!)
}
private var panSelectedView: UIView?
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
guard let selectedView = hitTest(gesture.location(in: self), with: nil) else { return }
panSelectedView = selectedView
if let movableAnnotation = selectedView as? MapMoveableAnnotation {
movableAnnotation.isHighlighted = true
}
case .changed:
guard let panSelectedView = panSelectedView else { return }
let touchPoint = gesture.location(in: self)
panSelectedView.center = touchPoint
delegate?.areaVertexMoved(index: panSelectedView.tag, point: touchPoint, isFinish: false)
case .ended:
if let movableAnnotation = panSelectedView as? MapMoveableAnnotation {
movableAnnotation.isHighlighted = false
if movableAnnotation.type == .areaControlPoint {
delegate?.areaVertexMoved(index: movableAnnotation.tag, point: movableAnnotation.center, isFinish: true)
}
delegate?.areaFinishChanging()
}
panSelectedView = nil
default:
break
}
}
}
複製代碼
這裏要強調的仍是每一個類職責的劃分。在架構中咱們常說單一職責,單一職責對於維護性的影響相當重要。咱們對 MapInteractionView 的定義就是接受用戶的交互事件。所以它在上面的場景中,處理移動手勢,通知外界哪一個頂點開始移動,移動結束。spa
如何檢驗單一職責執行的好很差呢?個人一個建議是思考這個模塊是否能夠抽離出來獨立驗證,也就是可測試性。以 MapInteractionView 爲例,如今這樣設計若是想要單獨測試這個類,能夠作到嗎?相對而言是比較容易的,能夠在單元測試項目中初始化添加到 VC 中,輸入特定的座標點,接着在模擬控制座標點位置模擬拖動手勢。在 MapInteractionDelegate 中斷言返回的結果和模擬的手勢值是否一致。架構設計
最後一環就是把 MapInteractionView 添加到地圖控件的最頂層,這塊代碼比較簡單代碼我就不列舉了。設計
複雜的業務必然會有「複雜」的實現。架構設計要解決的是:面對複雜的需求,複雜的實現應該讓開發者容易理解,容易維護。怎樣作到呢?咱們在規劃時就劃分好各個模塊,讓各個模塊經過合理的組合實現複雜的功能。這樣開發者在維護的時候只須要關注兩件事:1.模塊是如何劃分的 2.我要維護的部分在模塊中是如何實現的。每一個模塊由於職責清晰,因此即使不清楚全局,也能夠維護這個模塊的實現細節。這樣的設計就達到了易理解、已維護的目標。