ソースコード付きで中々思い通りに行かない地図上のタップイベントについて解決策を
解説します。
MapKitのMKMapView上で地図操作を邪魔せずにタッチイベントを取得するのは、
想像するよりもやっかいです。
通常ですと、タップの検知はtouchesBegan()等で行いますが、
MKMapViewでは既にタップのイベントが沢山用意されていますので、
単純にオーバーライド等で書き換えたとしても、マップの移動や拡大縮小など
本来MKMapViewが持っているタッチ操作を邪魔してしまいます。
また、Viewを別に用意してタップをそこで拾ったりを考えますが、
タップを拾った時点でイベントの通知がストップしてしまいますので、
中々上手く調整する事が出来ません。
そこで、UIViewのhittestを使用します。
今回の解説では、地図の操作を邪魔せずにタッチのイベントを取得し、
指定のメソッドを座標情報と共に起動する事を目的とします。
汎用的な作りにしてありますので、是非ともお使い下さい。
タッチを拾う為にUIViewを拡張しよう!
通常のUIViewをタッチを拾う為の受け皿になるように拡張します
内容としては、メンバとしてコールバックとして起動するメソッドのインスタンスとメソッドを保持し、
外部から汎用的に、このクラスを使用出来る様にします。
そして、タッチされた場所を示す座標もメンバに保持する様にしましたので、
起動されたコールバックから座標を取得することも出来ます。
それでは早速ヘッダの定義から
#import <Foundation/Foundation.h> @interface MapTouchView : UIView { // タッチされた座標 CGPoint touchPoint; // タッチがあった際に呼ばれるコールバック id callbackTarget; SEL callbackAction; } @property (nonatomic,assign) id callbackTarget; @property (nonatomic,assign) SEL callbackAction; @property (nonatomic,assign) CGPoint touchPoint; // タッチ検知後のコールバックを指定 -(void)setMapTouchCallbackTarget:(id)_target action:(SEL)_action; @end
各メンバを定義し、
タッチがあった際に呼ばれるメソッドをコールバックとして登録出来るメソッドを
インスタンスメソッドとして公開します。
続いて、MapTouchViewの実装部分です。
#import "MapTouchDetectionView.h" #include <time.h> // 連続処理を防ぐ為のタイマー値 static time_t last_date = 0; @implementation MapTouchView @synthesize touchPoint; @synthesize callbackTarget; @synthesize callbackAction; //------------------------------------------------------------------------------ // タップを検出し、座標を格納しながらコールバックを起動する //------------------------------------------------------------------------------ -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { // 連続タップを拒否する time_t t; time(&t); if( t==last_date ){ return( [super hitTest:point withEvent:event] ); } last_date = t; // タッチされたポイントを格納する self.touchPoint = point; // タッチ検出後のコールバックを起動する [self.callbackTarget performSelector:self.callbackAction withObject:self]; // MAP側のタップイベントが滞りなく行われるように、親の同メソッドを起動する return( [super hitTest:point withEvent:event] ); } //------------------------------------------------------------------------------ // タッチ検出後のコールバックを設定する //------------------------------------------------------------------------------ -(void)setMapTouchCallbackTarget:(id)_target action:(SEL)_action { // メンバにコールバック情報を保持する self.callbackTarget = _target; self.callbackAction = _action; } //------------------------------------------------------------------------------ // 破棄 //------------------------------------------------------------------------------ -(void)dealloc { self.callbackTarget = nil; [super dealloc]; } @end
UIViewのタップ検出イベントである
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)eventをオーバーライドし、座標等をメンバに格納しつつ設定されたコールバックメソッドを起動します。
そして、その後に親の同メソッドを叩き、他のタップイベントの続きを処理させます。
これでMKMapViewのタップイベントも滞りなく実行されます。
状況によっては
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)eventが数回呼ばれたり、連続したタッチを検出した場合に
コールバックが重複起動し無い様にタイマーも貼っていますが、
アプリの状況によってカスタマイズしてください。
タップを検知用ViewをMKMapViewに適用させる
受け皿となるViewを作成出来たらいよいよ使い方です。
内容と致しましては、先ほど作ったクラスをMKMapViewの親ViewとしてaddSubviewします
サンプルコードを下記に示したいと思います。
まずはヘッダから
#import#import #import "MapTouchView.h" @interface MapScene : UIViewController { // 地図のタッチ検知ビュー MapTouchView* mapTouchView; // 地図のビュー MKMapView* mapView; } @property (nonatomic,retain) MapTouchDetectionView* mapTouchView; @property (nonatomic,retain) MKMapView* mapView; @end
ヘッダで先程作成したMapTouchViewを読み込んでおきます。
実際の使い方は下記の実装コードをご覧下さい
#import "MapScene.h" @implementation MapScene @synthesize mapTouchView; @synthesize mapView; //------------------------------------------------------------------------------ // タッチ検出後に起動させるコールバックメソッド //------------------------------------------------------------------------------ -(void)mapTouchEvent:(MapTouchView*)_MapTouchView { // タッチされた座標情報 float x = _MapTouchView.touchPoint.x; float y = _MapTouchView.touchPoint.y; } //------------------------------------------------------------------------------ // viewの構築 //------------------------------------------------------------------------------ -(void)viewDidLoad { [super viewDidLoad]; // タッチ検出用のオーバービューを作成する self.mapTouchView = nil; self.mapTouchView = [[[MapTouchView alloc] init] autorelease]; self.mapTouchView.frame = CGRectMake( 0, 0, self.view.frame.size.width, self.view.frame.size.height ); [self.view addSubview:self.mapTouchView]; // 地図をタッチ検出用のオーバービューの上に作成 self.mapView = nil; self.mapView = [[[MKMapView alloc] init] autorelease]; self.mapView.frame = CGRectMake( 0, 0, self.view.frame.size.width, self.view.frame.size.height ); self.mapView.delegate = self; [self.mapTouchView addSubview:self.mapView]; // マップタッチのコールバックを設定する [self.mapTouchView setMapTouchCallbackTarget:self action:@selector(mapTouchEvent:)]; } //------------------------------------------------------------------------------ // 破棄 //------------------------------------------------------------------------------ -(void)dealloc { self.mapView = nil; self.mapTouchView = nil; [super dealloc]; } @end
先ほど作成したMapTouchViewの現在のviewへ追加します。
そして、MapTouchViewの上にMKMapViewを配置します。
マップがタッチされた時に起動したいメソッドを
先ほどのクラス内でインスタンスメソッドとして定義した
-(void)setMapTouchCallbackTarget:(id)_target action:(SEL)_action;でセットします。
また、MapTouchViewはMKMapViewと同じサイズにする事によって
タッチされた座標が地図上の座標と一致しますので便利です。
後はタップが検知されれば設定したコールバックメソッドが起動されます。
コールバックメソッドでは引数としてMapTouchViewを返却してくるので、
その中にメンバで持っている座標情報を参照する事で
値を取得出来ます。
上記の様に実装する事によって、MKMapView上でも操作を邪魔する事無く
タップ位置を取得し、タップイベントを起動する事が出来ます。