前回表示した地図画面をいくら長押ししても、地図アプリでおなじみのピンは落ちてきません。
これね
長押しでピンが落ちてこられても困るアプリケーションもあるわけで、まあ、妥当な初期設定ではあるわけですよ。ちなみにズームや移動もそれぞれ
で無効にできます。こうなると、完全に地図表示だけのビューになるわけです。
ってすると衛星写真版にもなる。
どやっ
ま、それはいいとして、じゃ、ピン落とすにはどうするかっていうと、MKMapViewに落とすべき位置を教えてやる必要があるわけですよ。
この位置情報をMKMapViewに伝えるのに使うのが
でして、このインスタンスを用意して、位置を例の緯度、経度で指定した後MKMapViewにaddAnnotation:メッセージで追加するわけです。
例えば、前回の中心位置として指定した
にピンを落とすならこんな感じ
これでRunさせると
てな感じでピンが付きます。
「え、でもピン落ちるアニメーションは?」という贅沢な人は、もうちょっと工夫が必要です。
というのも、MKPointAnnotationてのは、あくまで位置情報にすぎず、画面上のビューとしては別のインスタンスが必要とされてまして、何もしなければ
ってのが、MKMapView内で、必要になった時に生成されて利用されるようになっているんですな。
この点は、ドリルでやったUITableViewクラスとUITableViewCellクラスの関係を思い出してくれたらいいです。MKMapViewがスクロールしてMKPointAnnotationが示す位置が画面上に表示されたら、MKPointAnnotationViewを作成して貼付けるわけです。で、スクロールアウトしたら取り外す。
でもって、ピンが地図に追加(表示じゃなく追加のタイミングね。表示だとスクロールアウトして、再度スクロールインした時も落ちちゃうから)される時に、ピンが落ちるアニメーションをおこなわせるには、このMKPointAnnotationViewインスタンスのプロパティ
をYESにしておく必要があるんですが、上で説明したように、内部で必要なタイミングの時に作成されてるので、そのままだと外部からは手が出せないんですよ。内部に潜り込むためには~
デリゲートちゃんの出番なわけです。MKMapViewのデリゲート用メソッドはMKMapViewDelegateプロトコルにまとめられてて、全部@optionalなんで、必要なメソッドだけ用意すればいい事になってます。
今回の場合は
というメソッドです。
やり方はUITableViewと同じで、MKMapViewの場合はMKAnnotationViewまたは、その派生クラスのインスタンスを返す必要があります。最初にリサイクルキューに任意の識別子に一致するMKAnnotationView(その派生)インスタンスが無いか探し、あれば、それを利用、なければ作成ってとこも同じ考え。
まずはKMViewControllerにMKMapViewDelegateを採用しておいて、_mapViewのdelegateに自分を設定します。
ちなみにクラス拡張ではインスタンス変数宣言だけじゃなくプロトコル採用の指定もできるんですよ~、ヘッダーファイルからは完全に隠蔽できます。
で、MKMapViewDelegateのメソッド実装を追加
リサイクル可能なMKPinAnnotationViewインスタンスをMKMapViewからdequeueReusableAnnotationViewWithIdentifier:で探して、あればそいつを利用、なければnilが返るんで、あらためてインスタンスを作成。その時にanimatesDropにYESを設定する。
これでピンが落ちてくるわけですよ。
Runしてみて確認してみてください。
-viewDidLoadでaddAnnotation:やっちゃてるので、タイミングがはやすぎて地図が表示される前にピンが落ちますな。
-viewDidAppear:やMKMapViewDelegate側の– mapView:regionDidChangeAnimated:、– mapViewDidFinishLoadingMap:でaddAnnotation:をやるってのでもいいけど、これらのメソッドは初期化時一回だけってわけじゃないんで、その対応が必要なのと、結局確実に地図を表示してからピンを落とすのは難しいみたい。
Grand Central Dispatch使って2秒後に自動でピンを落とすとか…まあ、これはこれで画面切り替えされた時の対応とか考えんといかんわけだけど、_mapViewが存在してる限りは大丈夫だし、_mapViewがnilになっている場合も問題はないでしょう。MKPointAnnotationを作って、そのまま破棄する事になるわけだが~
て感じでやってください。
Grand Central Dispatchの詳細については、並列処理の方でいずれ。
次に、地図アプリみたいに画面上を押して、ピンを落とすにはUILongPressGestureRecognizerを使います。UILongPressGestureRecognizerインスタンスを作ってMKMapViewに追加して画面長押しをターゲットアクションデザインパターン使って見張るわけですよ。こいつもドリルで取り上げたUITapGestureRecognizerと同じです。
MKPointAnnotationに設定する緯度経度を知るには、まずUILongPressGestureRecognizerインスタンスからlocationInView:メッセージを使って画面上のタップ位置を取り出します。この時、どの画面上のローカル座標かを伝えるため画面インスタンスを指定する必要があるんだけど、tapGestureRecognizer.viewを使っても_mapView使ってもどっちでも結果は同じです。
で、このタップ位置をCLLocationCoordinate2DにするにはMKMapViewにconvertPoint:toCoordinateFromView:メッセージを送ればいいわけで
とします。ここでも渡した座標がどの画面上のローカル座標かを特定するためtoCoordinateFromView:にgestureRecognizer.viewを指定してる。結局、_mapView上のローカル座標を緯度経度に変換してるわけです。
あとは、さっきと同じ要領でMKPointAnnotationインスタンス作成してaddAnnotation:しちゃえばいいわけです。
気をつけるのはUILongPressGestureRecognizerは長押し中、一定間隔(minimumPressDurationプロパティで指定)で繰り返しアクションに指定された(-tap:)メソッドを呼び出してくるので、そのままだと指が放されるまで同じ場所にいくつもMKPointAnnotationを追加する事になるんですな。それを防止するために今回は最初に
とやって、長押し中、最初の1回だけ実行するようにしています。
UIGestureRecognizerのプロパティstateは繰り返し呼び出される場合、最初の1回目がUIGestureRecognizerStateBegan、その後の呼び出しの繰り返し中はUIGestureRecognizerStateChanged、最後の呼び出しでUIGestureRecognizerStateEndedと変化するので、こいつでピンを落とすタイミングを調整できたりするわけです。
例えばUIGestureRecognizerStateEndedで実行するようにすれば、指が放れた時にピンが落ちるようになるんだけど、これはちょい変でした。
ちなみに、ピンをタップすると出る吹き出し(コールアウトって言います)
を出したいならMKPointAnnotationViewのプロパティcanShowCalloutをYESにする必要と、MKPointAnnotationのプロパティtitleに文字列が指定されている必要があります。
これで、最初に自動で落とすピンはtitleを設定してないのでいくらタップしてもコールアウトは出ずに、長押しして落としたピンは出るはず。
あと、ピンのドラッグは、iOS 5.0からMKPointAnnotationViewのプロパティdraggableをYESで済むようになりますた。グレート。
よく見かけるコールアウトのデスクロージャ
はMKPinAnnotationViewインスタンスを作った時にプロパティrightCalloutAccessoryViewに
てする事で追加されマッスル。
この時、UIButtonのターゲットアクションを指定する事もできるんだけど、その場合、複数のMKPointAnnotationViewを表示している場合に、どのMKPointAnnotationViewのUIButtonのタップかを特定するのが面倒なんですよ。
簡単なのはMKMapViewのデリゲート側を使う方法。
以上、地図にピンを落とすでした~。
次回は地図に、ピンではなく独自の注釈を付ける方法。
------------
サンプルプロジェクト:KyotoMap-2.zip
これね
長押しでピンが落ちてこられても困るアプリケーションもあるわけで、まあ、妥当な初期設定ではあるわけですよ。ちなみにズームや移動もそれぞれ
_mapView.zoomEnabled = NO;
_mapView.scrollEnabled = NO;
_mapView.scrollEnabled = NO;
で無効にできます。こうなると、完全に地図表示だけのビューになるわけです。
_mapView.mapType = MKMapTypeSatellite;
ってすると衛星写真版にもなる。
どやっ
ま、それはいいとして、じゃ、ピン落とすにはどうするかっていうと、MKMapViewに落とすべき位置を教えてやる必要があるわけですよ。
この位置情報をMKMapViewに伝えるのに使うのが
MKPointAnnotationクラス
でして、このインスタンスを用意して、位置を例の緯度、経度で指定した後MKMapViewにaddAnnotation:メッセージで追加するわけです。
例えば、前回の中心位置として指定した
35.0212466
135.7555968
135.7555968
にピンを落とすならこんな感じ
- (void)viewDidLoad
{
・・・
_mapView.region = kyotoregion; // アニメーション抜き
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = CLLocationCoordinate2DMake(35.0212466, 135.7555968);
[_mapView addAnnotation:pin];
}
{
・・・
_mapView.region = kyotoregion; // アニメーション抜き
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = CLLocationCoordinate2DMake(35.0212466, 135.7555968);
[_mapView addAnnotation:pin];
}
これでRunさせると
てな感じでピンが付きます。
「え、でもピン落ちるアニメーションは?」という贅沢な人は、もうちょっと工夫が必要です。
というのも、MKPointAnnotationてのは、あくまで位置情報にすぎず、画面上のビューとしては別のインスタンスが必要とされてまして、何もしなければ
MKPointAnnotationView
ってのが、MKMapView内で、必要になった時に生成されて利用されるようになっているんですな。
この点は、ドリルでやったUITableViewクラスとUITableViewCellクラスの関係を思い出してくれたらいいです。MKMapViewがスクロールしてMKPointAnnotationが示す位置が画面上に表示されたら、MKPointAnnotationViewを作成して貼付けるわけです。で、スクロールアウトしたら取り外す。
でもって、ピンが地図に追加(表示じゃなく追加のタイミングね。表示だとスクロールアウトして、再度スクロールインした時も落ちちゃうから)される時に、ピンが落ちるアニメーションをおこなわせるには、このMKPointAnnotationViewインスタンスのプロパティ
animatesDrop
をYESにしておく必要があるんですが、上で説明したように、内部で必要なタイミングの時に作成されてるので、そのままだと外部からは手が出せないんですよ。内部に潜り込むためには~
デリゲートちゃんの出番なわけです。MKMapViewのデリゲート用メソッドはMKMapViewDelegateプロトコルにまとめられてて、全部@optionalなんで、必要なメソッドだけ用意すればいい事になってます。
今回の場合は
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation;
というメソッドです。
やり方はUITableViewと同じで、MKMapViewの場合はMKAnnotationViewまたは、その派生クラスのインスタンスを返す必要があります。最初にリサイクルキューに任意の識別子に一致するMKAnnotationView(その派生)インスタンスが無いか探し、あれば、それを利用、なければ作成ってとこも同じ考え。
まずはKMViewControllerにMKMapViewDelegateを採用しておいて、_mapViewのdelegateに自分を設定します。
ちなみにクラス拡張ではインスタンス変数宣言だけじゃなくプロトコル採用の指定もできるんですよ~、ヘッダーファイルからは完全に隠蔽できます。
@interface KMViewController ()<MKMapViewDelegate> {
MKMapView* _mapView;
}
@end
- (void)viewDidLoad
{
[super viewDidLoad];
_mapView = [[MKMapView alloc] initWithFrame:self.view.frame];
_mapView.delegate = self;
・・・
}
MKMapView* _mapView;
}
@end
- (void)viewDidLoad
{
[super viewDidLoad];
_mapView = [[MKMapView alloc] initWithFrame:self.view.frame];
_mapView.delegate = self;
・・・
}
で、MKMapViewDelegateのメソッド実装を追加
- (MKAnnotationView *)mapView:(MKMapView *)mapView
viewForAnnotation:(id)annotation
{
static NSString* Identifier = @"PinAnnotationIdentifier";
MKPinAnnotationView* pinView = (MKPinAnnotationView *)[mapView
dequeueReusableAnnotationViewWithIdentifier:Identifier];
if (pinView == nil) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:Identifier];
pinView.animatesDrop = YES;
return pinView;
}
pinView.annotation = annotation;
return pinView;
}
viewForAnnotation:(id
{
static NSString* Identifier = @"PinAnnotationIdentifier";
MKPinAnnotationView* pinView = (MKPinAnnotationView *)[mapView
dequeueReusableAnnotationViewWithIdentifier:Identifier];
if (pinView == nil) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:Identifier];
pinView.animatesDrop = YES;
return pinView;
}
pinView.annotation = annotation;
return pinView;
}
リサイクル可能なMKPinAnnotationViewインスタンスをMKMapViewからdequeueReusableAnnotationViewWithIdentifier:で探して、あればそいつを利用、なければnilが返るんで、あらためてインスタンスを作成。その時にanimatesDropにYESを設定する。
これでピンが落ちてくるわけですよ。
Runしてみて確認してみてください。
-viewDidLoadでaddAnnotation:やっちゃてるので、タイミングがはやすぎて地図が表示される前にピンが落ちますな。
-viewDidAppear:やMKMapViewDelegate側の– mapView:regionDidChangeAnimated:、– mapViewDidFinishLoadingMap:でaddAnnotation:をやるってのでもいいけど、これらのメソッドは初期化時一回だけってわけじゃないんで、その対応が必要なのと、結局確実に地図を表示してからピンを落とすのは難しいみたい。
Grand Central Dispatch使って2秒後に自動でピンを落とすとか…まあ、これはこれで画面切り替えされた時の対応とか考えんといかんわけだけど、_mapViewが存在してる限りは大丈夫だし、_mapViewがnilになっている場合も問題はないでしょう。MKPointAnnotationを作って、そのまま破棄する事になるわけだが~
- (void)viewDidLoad
{
・・・
_mapView.region = kyotoregion; // アニメーション抜き
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = center;
[_mapView addAnnotation:pin];
});
}
{
・・・
_mapView.region = kyotoregion; // アニメーション抜き
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.coordinate = center;
[_mapView addAnnotation:pin];
});
}
て感じでやってください。
Grand Central Dispatchの詳細については、並列処理の方でいずれ。
次に、地図アプリみたいに画面上を押して、ピンを落とすにはUILongPressGestureRecognizerを使います。UILongPressGestureRecognizerインスタンスを作ってMKMapViewに追加して画面長押しをターゲットアクションデザインパターン使って見張るわけですよ。こいつもドリルで取り上げたUITapGestureRecognizerと同じです。
- (void)viewDidLoad
{
・・・
[_mapView addAnnotation:pin];
});
// 長押し見張り
UILongPressGestureRecognizer* tapGestureRecognizer
= [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
[_mapView addGestureRecognizer:tapGestureRecognizer];
}
// 地図タップ対応
- (void)tap:(UILongPressGestureRecognizer*)gestureRecognizer
{
ここでMKPointAnnotationを追加
}
{
・・・
[_mapView addAnnotation:pin];
});
// 長押し見張り
UILongPressGestureRecognizer* tapGestureRecognizer
= [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
[_mapView addGestureRecognizer:tapGestureRecognizer];
}
// 地図タップ対応
- (void)tap:(UILongPressGestureRecognizer*)gestureRecognizer
{
ここでMKPointAnnotationを追加
}
MKPointAnnotationに設定する緯度経度を知るには、まずUILongPressGestureRecognizerインスタンスからlocationInView:メッセージを使って画面上のタップ位置を取り出します。この時、どの画面上のローカル座標かを伝えるため画面インスタンスを指定する必要があるんだけど、tapGestureRecognizer.viewを使っても_mapView使ってもどっちでも結果は同じです。
CGPoint pt = [gestureRecognizer locationInView:gestureRecognizer.view];
で、このタップ位置をCLLocationCoordinate2DにするにはMKMapViewにconvertPoint:toCoordinateFromView:メッセージを送ればいいわけで
CLLocationCoordinate2D coordinate = [_mapView convertPoint:pt
toCoordinateFromView:gestureRecognizer.view];
toCoordinateFromView:gestureRecognizer.view];
とします。ここでも渡した座標がどの画面上のローカル座標かを特定するためtoCoordinateFromView:にgestureRecognizer.viewを指定してる。結局、_mapView上のローカル座標を緯度経度に変換してるわけです。
あとは、さっきと同じ要領でMKPointAnnotationインスタンス作成してaddAnnotation:しちゃえばいいわけです。
気をつけるのはUILongPressGestureRecognizerは長押し中、一定間隔(minimumPressDurationプロパティで指定)で繰り返しアクションに指定された(-tap:)メソッドを呼び出してくるので、そのままだと指が放されるまで同じ場所にいくつもMKPointAnnotationを追加する事になるんですな。それを防止するために今回は最初に
- (void)tap:(UILongPressGestureRecognizer*)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateBegan)
return;
・・・
{
if (gestureRecognizer.state != UIGestureRecognizerStateBegan)
return;
・・・
とやって、長押し中、最初の1回だけ実行するようにしています。
UIGestureRecognizerのプロパティstateは繰り返し呼び出される場合、最初の1回目がUIGestureRecognizerStateBegan、その後の呼び出しの繰り返し中はUIGestureRecognizerStateChanged、最後の呼び出しでUIGestureRecognizerStateEndedと変化するので、こいつでピンを落とすタイミングを調整できたりするわけです。
例えばUIGestureRecognizerStateEndedで実行するようにすれば、指が放れた時にピンが落ちるようになるんだけど、これはちょい変でした。
ちなみに、ピンをタップすると出る吹き出し(コールアウトって言います)
を出したいならMKPointAnnotationViewのプロパティcanShowCalloutをYESにする必要と、MKPointAnnotationのプロパティtitleに文字列が指定されている必要があります。
- (void)tap:(UILongPressGestureRecognizer*)gestureRecognizer
{
・・・
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.title = @"京都";
・・・
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation
{
・・・
if (pinView == nil) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:Identifier];
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
・・・
}
{
・・・
MKPointAnnotation* pin = [[MKPointAnnotation alloc] init];
pin.title = @"京都";
・・・
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id
{
・・・
if (pinView == nil) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:Identifier];
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
・・・
}
これで、最初に自動で落とすピンはtitleを設定してないのでいくらタップしてもコールアウトは出ずに、長押しして落としたピンは出るはず。
あと、ピンのドラッグは、iOS 5.0からMKPointAnnotationViewのプロパティdraggableをYESで済むようになりますた。グレート。
よく見かけるコールアウトのデスクロージャ
はMKPinAnnotationViewインスタンスを作った時にプロパティrightCalloutAccessoryViewに
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
pinView.rightCalloutAccessoryView = rightButton;
pinView.rightCalloutAccessoryView = rightButton;
てする事で追加されマッスル。
この時、UIButtonのターゲットアクションを指定する事もできるんだけど、その場合、複数のMKPointAnnotationViewを表示している場合に、どのMKPointAnnotationViewのUIButtonのタップかを特定するのが面倒なんですよ。
簡単なのはMKMapViewのデリゲート側を使う方法。
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation
{
・・・
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
pinView.draggable = YES;
// デスクロージャ追加
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
pinView.rightCalloutAccessoryView = rightButton;
・・・
}
// デスクロージャタップ対応
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
printf("tap DetailDisclosure\n");
}
{
・・・
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
pinView.draggable = YES;
// デスクロージャ追加
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
pinView.rightCalloutAccessoryView = rightButton;
・・・
}
// デスクロージャタップ対応
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
printf("tap DetailDisclosure\n");
}
以上、地図にピンを落とすでした~。
次回は地図に、ピンではなく独自の注釈を付ける方法。
------------
サンプルプロジェクト:KyotoMap-2.zip