画面端からのスワイプジェスチャーでMapKitの地図表示をズームさせるサンプルプログラムです。
つくったもの
画面左右の端からのスワイプ量に応じて地図がぬるぬるっとズームイン・ズームアウトします。現在地を表示するカスタムピンも実装してみました。
環境
- Xcode 15.2
- iOS 17.2.1
実装ポイント
MapKitのMKMapViewのタップジャスチャーをインターセプト
MKMapViewのタップイベントをトリガーにズームするとMapKitの挙動と干渉してしまって、ズームがカクついてしまいます。代わりにタップジャスチャーを上位Viewでインターセプトすることでぬぬるぬる動作するズームが実装できました。
ViewController.swift
class ViewController: UIViewController {
var mapView: MKMapView!
var locationManager: CLLocationManager!
var tapAreaView: TapAreaView!
var isLoadingMap = false
var isRenderingMap = false
var lastTapPoint = CGPoint(x: 0, y: 0)
var lastEventTimestamp: TimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
setupMapKit()
setupZoomArea(rect: view.bounds)
}
private func setupMapKit() {
// mapViewの生成
mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
mapView.delegate = self
mapView.showsBuildings = true
mapView.showsScale = true
mapView.showsCompass = true
//mapView.showsUserLocation = true
self.view.addSubview(mapView)
// ロケーションマネージャーのセットアップ
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
// 東京駅に照準を合わす
let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
let coordinate = CLLocationCoordinate2DMake(35.681236, 139.767125)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapView.region = region
// pinに表示する画像(吹き出し画像で切り抜く)
let sourceImage = UIImage(named: "profile1")
let maskImage = UIImage(named: "balloon_mask")
let image = sourceImage?.masking(maskImage: maskImage)
// pin生成
let pin = PlaceAnnotation(index: 1, name: "name1", image: image!, latitude: 35.681236, longitude: 139.767125)
pin.coordinate = coordinate
mapView.addAnnotation(pin)
// ドラッグ検知
let tapInterceptor = GlobalGestureRecognizer(target: nil, action: nil)
tapInterceptor.touchesBeganCallback = { touches, event in
let touchPoint = touches.first?.location(in: self.view) ?? CGPoint()
self.lastTapPoint = touchPoint
var x = self.view.bounds.width * 0.2
let hitLeftRect = CGRect(x: 0, y: 0, width: x, height: self.view.bounds.height)
x = self.view.bounds.width * 0.8
let hitRightRect = CGRect(x: x, y: 0, width: self.view.bounds.width - x, height: self.view.bounds.height)
if hitLeftRect.contains(touchPoint) || hitRightRect.contains(touchPoint){
self.mapView.isScrollEnabled = false
self.tapAreaView.showArea(touchPoint: touchPoint)
}
}
tapInterceptor.touchesMovedCallback = { touches, event in
if !self.tapAreaView.isHidden {
self.tapAreaView.moveAreaWithCallback(touches: touches, event: event)
}
}
tapInterceptor.touchesEndedCallback = { touches, event in
if !self.tapAreaView.isHidden {
self.mapView.isScrollEnabled = true
self.tapAreaView.hideArea()
}
}
mapView.addGestureRecognizer(tapInterceptor)
}
private func setupZoomArea(rect: CGRect) {
let drawView = TapAreaView(frame: rect)
drawView.hideArea()
self.view.addSubview(drawView)
self.tapAreaView = drawView
drawView.touchesBeganCallback = { touches, event in
self.mapView.isScrollEnabled = false
self.lastTapPoint = touches.first?.location(in: self.mapView) ?? CGPoint()
}
drawView.touchesEndedCallback = { touches, event in
self.mapView.isScrollEnabled = true
}
drawView.touchesMovedCallback = { touches, event in
if self.isLoadingMap || self.isRenderingMap {
return
}
if self.mapView.isScrollEnabled {
return
}
if event.timestamp - self.lastEventTimestamp < 0.1 {
return
}
self.lastEventTimestamp = event.timestamp
let tapPoint = touches.first?.location(in: self.mapView) ?? CGPoint()
let deltaX = self.lastTapPoint.x - tapPoint.x
let deltaY = self.lastTapPoint.y - tapPoint.y
self.lastTapPoint = tapPoint
var zoomValue = 2.0
if abs(deltaX) > abs(deltaY) {
if abs(deltaX) < 3 {
return
}
if deltaX < 0 {
zoomValue *= -1
}
} else {
if abs(deltaY) < 3 {
return
}
if deltaY < 0 {
zoomValue *= -1
}
}
self.zoomMap(zoomValue: zoomValue)
}
}
private func zoomMap(zoomValue: Double) {
var region: MKCoordinateRegion = self.mapView.region
if zoomValue < 0 {
region.span.latitudeDelta /= abs(zoomValue)
region.span.longitudeDelta /= abs(zoomValue)
} else {
region.span.latitudeDelta *= zoomValue
region.span.longitudeDelta *= zoomValue
}
if region.span.latitudeDelta < 0 || region.span.latitudeDelta > 30 {
return
}
if region.span.longitudeDelta < 0 || region.span.longitudeDelta > 30 {
return
}
self.isRenderingMap = true
self.mapView.setRegion(region, animated: true)
}
}
グローバルなジェスチャーイベント処理
Stack Overflowに投稿されていた「How to intercept touches events on a MKMapView or UIWebView objects?」を参考にジェスチャーイベントをグローバル化するコードを実装しました。
GlobalGestureRecognizer.swift
import UIKit
class GlobalGestureRecognizer: UIGestureRecognizer {
var touchesBeganCallback: ((Set<UITouch>, UIEvent) -> Void)?
var touchesEndedCallback: ((Set<UITouch>, UIEvent) -> Void)?
var touchesMovedCallback: ((Set<UITouch>, UIEvent) -> Void)?
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.cancelsTouchesInView = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let touchesBeganCallback {
touchesBeganCallback(touches, event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if let touchesEndedCallback {
touchesEndedCallback(touches, event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touchesMovedCallback {
touchesMovedCallback(touches, event)
}
}
override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
ズーム量をUIフィードバック
タップ位置とズーム量が視覚的にわかるようにTapAreaViewをオーバーレイさせます。タッチ座標をcontrolPointとするベジェ曲線を描画し、画面端との閉域をグレー色で塗ります。
TapAreaView.swift
import UIKit
class TapAreaView: UIView {
var touchesBeganCallback: ((Set<UITouch>, UIEvent) -> Void)?
var touchesEndedCallback: ((Set<UITouch>, UIEvent) -> Void)?
var touchesMovedCallback: ((Set<UITouch>, UIEvent) -> Void)?
var isLeftArea = true
private var parentRect: CGRect!
private var drawRect: CGRect!
private var touchPoint: CGPoint = CGPoint()
override init(frame: CGRect) {
parentRect = frame
drawRect = frame
super.init(frame: frame);
self.backgroundColor = UIColor.clear;
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
var startPoint = CGPoint()
var endPoint = CGPoint()
var controlPoint = CGPoint()
if isLeftArea {
startPoint = CGPoint(x: 0, y: 0)
endPoint = CGPoint(x: 0, y: parentRect.height)
let deltaX = touchPoint.x
controlPoint = touchPoint
controlPoint.x += deltaX
} else {
startPoint = CGPoint(x: parentRect.width, y: 0)
endPoint = CGPoint(x: parentRect.width, y: parentRect.height)
let deltaX = parentRect.width - touchPoint.x
controlPoint = touchPoint
controlPoint.x -= deltaX
}
let path = UIBezierPath()
path.move(to: startPoint)
path.addQuadCurve(to: endPoint,
controlPoint: CGPoint(x: controlPoint.x, y: controlPoint.y))
UIColor.black.withAlphaComponent(0.3).setFill()
path.fill()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
showArea(touchPoint: location)
if let touchesBeganCallback {
touchesBeganCallback(touches, event!)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
hideArea()
if let touchesEndedCallback {
touchesEndedCallback(touches, event!)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
moveArea(touchPoint: location)
if let touchesMovedCallback {
touchesMovedCallback(touches, event!)
}
}
func showArea(touchPoint: CGPoint) {
let x = touchPoint.x
self.drawRect = CGRect(x: x, y: parentRect.origin.y, width: parentRect.width - x, height: parentRect.height)
self.touchPoint = touchPoint
if x < (parentRect.width / 2) {
isLeftArea = true
} else {
isLeftArea = false
}
setNeedsDisplay()
layer.opacity = 0.9
}
func hideArea() {
layer.opacity = 0.0
}
func moveArea(touchPoint: CGPoint) {
let x = touchPoint.x
self.drawRect = CGRect(x: x, y: parentRect.origin.y, width: parentRect.width - x, height: parentRect.height)
self.touchPoint = touchPoint
setNeedsDisplay()
}
func moveAreaWithCallback(touches: Set<UITouch>, event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
let x = location.x
self.drawRect = CGRect(x: x, y: parentRect.origin.y, width: parentRect.width - x, height: parentRect.height)
self.touchPoint = location
setNeedsDisplay()
if let touchesMovedCallback {
touchesMovedCallback(touches, event!)
}
}
}
完成!
GitHub