2010年2月14日日曜日

MKMapViewのタップ位置を取得する

MKMapViewでタップされた位置を取得するのは意外と難しい事だった。

MKMapViewを拡張する方法 - ダメ

MKMapViewはUIResponderのサブクラスなので、当然touchesBegan()などでタッチイベントを取得出来るだろうと思った。しかし、実際にMKMapViewのサブクラスを作ってtouchesBegan()でタッチイベントを取れるかどうか試してみると、まったく取得できない。

MKMapViewはそれ自体でスクロールやズームをサポートしているので、その関連でタッチイベント関連が特殊な動作になっているのかもしれない。

ドキュメントを見ても「・・・Although you should not subclass the MKMapView class itself・・・」とあるので、MKMapViewを拡張するのはやめておいた方が良さそうだ。


透明なUIViewを地図の上に重ねる方法 - ダメ

次に思いついたのが、透明なUIViewを地図の上に重ねる方法。これならタッチイベントは完璧に取得できるが、UIViewが透過していてもその下にあるMKMapViewまで到達しないので、スクロールやズーム操作が全くできない事がわかった。


透明なUIViewを地図の下に重ねる方法 - ダメ

さらに思いついたのは、透明なUIViewを地図の下に重ねる方法。この場合は、タッチイベントがMKMapViewで止まってしまい、その下にあるUIViewまで到達しない。まぁこれは予想通りだった。


UIView.hitTest()をオーバーライドして使う方法 - もう少し!

他に方法は無いか調べてみると、皆結構同じ問題で悩んでいるようだったが、一つだけ解決方法か?と思える記事を見つけた。
How to intercept touches events on a MKMapView or UIWebView objects? - Stack Overflow

このMartinさんのアイディアは、touchesBegan()などのタッチイベントを取得するのではなく、UIView.hitTest()をオーバーライドして使うというものだ。実際にやってみると・・・取れた!

ほぼ問題なく動作するのだが、一つだけ問題があった。記事のコメントでも指摘されているように、地図のズームの動作ができないのだ。どうやらマルチタッチ関連の問題があるようだ。原因は不明。


UIView.hitTest()をオーバーライドして使う方法(改) - YES!

しかし、開発中のアプリでは地図のズームは必須機能なので、なんとかならないか試行錯誤することに。

アプリでは単に地図上でタップされた位置が取得できれば良いので、Martinさんのアイディアを少し変えてこうしてやった!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  NSLog(@"hitTest() x=%f y=%f", point.x, point.y);
  return [super hitTest:point withEvent:event];
}

TapIntercepterViewなど適当な名前のUIViewのサブクラスを作り、上のメソッドを追加して、地図をサブビューとして追加する。TapIntercepterViewと地図の位置とサイズをピッタリ合わせれば、MKMapViewでタップ位置を取得する事と同じような動作になる。

MatinさんのアイディアではhitTest()で自分自身を返していたが、この方法ではタップ位置を取得しつつ、hitTest()で本来行われていることをそのまま行う。そのため、地図のスクロールもズームも邪魔されないというわけだ。

Thank you very much, Matin!