iOSで地図上にカスタム吹出し(Cutom callout)を表示する方法

iOSでは地図上にピンを立てて、そのピンをクリックすると吹出しを表示することができます。

設定方法は下記の様な感じです。

annotationView.canShowCallout = YES;

ただ、この吹出し(Callout)を独自のViewにする方法は無いかなと調べてみました。

MKMapViewにaddSubviewして表示すれば良いじゃね?っと思ったのですが、addSubviewだと地図を移動したときに吹出しが連動して動かないんですね。。。

で、下記のサイトにカスタムコールアウトの表示方法が記載されていました。

ただ、このサンプルだと複数ピンにたいして複数コールアウトが。。。

Building Custom Map Annotation Callouts – Part 1

そこで、サンプルコードを書いてみました。

ポイントは先ほどのサイトで記載されていますが、Callout viewもMKAnnotationViewとして扱う、ですね。

ポイントだけ書くので後はサンプルソースを見てください。

まずは通常のピン用のMKAnnotationを「PinAnnotation」として作ります。

プロパティは3つ。

吹出しに表示するタイトル「title」

経度緯度「coordinate」

吹出しのMKAnnotation「calloutAnnotation」

@interface PinAnnotation : NSObject <MKAnnotation>
…
@property (nonatomic, retain) NSString *title;
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, retain) CalloutAnnotation *calloutAnnotation;
@end

次に、吹出し用のMKAnnotationを「CalloutAnnotation」として作ります。

プロパティは2つ。

吹出しに表示するタイトル「title」

経度緯度「coordinate」

@interface CalloutAnnotation : NSObject <MKAnnotation>
…
@property (nonatomic, retain) NSString *title;
@property (nonatomic) CLLocationCoordinate2D coordinate;
@end

最後に吹出し用に「MKAnnotationView」を継承した「CalloutAnnotationView」を作ります。

プロパティは1つ。

吹出しに表示するタイトル「title」

@interface CalloutAnnotationView : MKAnnotationView
…
@property (nonatomic, retain) NSString *title;
@end

で、ピンのアノテーションをつくって地図にaddAnnotationするときに、pinAnnotation.title経由で吹出しに表示したいタイトルをセットして渡します。

pinAnnotation.title = (NSString *)[location objectForKey:@”title”];
[mapView_ addAnnotation:pinAnnotation];

MKMapViewのdelegateでピンが選択されたときに呼ばれる「- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view」で選択されたMKAnnotationがPinAnnotationだったらCalloutAnnotationを生成します。 生成したCalloutAnnotationにタイトルと経度緯度を渡します。 ピンの選択が解除されたときには吹出しを消したいので、生成したCalloutAnnotationをPinAnnotationにも渡しておきます。 生成したCalloutAnnotationをMKMapViewにaddAnnotationします。

– (void)mapView:(MKMapView *)mapView
didSelectAnnotationView:(MKAnnotationView *)view {
  if ([view.annotation isKindOfClass:[PinAnnotation class]]) {
    // Selected the pin annotation.
    CalloutAnnotation *calloutAnnotation = [[[CalloutAnnotation alloc] init] autorelease];
    PinAnnotation *pinAnnotation = ((PinAnnotation *)view.annotation);
    calloutAnnotation.title = pinAnnotation.title;
    calloutAnnotation.coordinate = pinAnnotation.coordinate;
    pinAnnotation.calloutAnnotation = calloutAnnotation;
    [mapView addAnnotation:calloutAnnotation];
  }
}

CalloutAnnotationが地図に追加されると「- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation」が呼ばれます。

MKAnnotationがCalloutAnnotationだったらCalloutAnnotationViewを生成します。

生成したCalloutAnnotationView.titleにCalloutaAnnotationのタイトルをセットします。

MKAnnotationViewは使い回しされるのでそのままだと表示する文字などが更新されないので「setNeedsDisplay」で強制的に更新します。

また、標準の吹出しだと自動的に地図の表示位置がスクロールされて良きに計らってくれますが、カスタムコールアウトでは「mapView.centerCoordinate = calloutAnnotation.coordinate」などで表示位置を調整する必要があります。

– (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
  MKAnnotationView *annotationView;
  NSString *identifier;
  if ([annotation isKindOfClass:[PinAnnotation class]]) {
    // Pin annotation.
    …
  } else if ([annotation isKindOfClass:[CalloutAnnotation class]]) {
    // Callout annotation.
    identifier = @”Callout”;
    annotationView = (CalloutAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
    if (annotationView == nil) {
      annotationView = [[[CalloutAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier] autorelease];
    }
    CalloutAnnotation *calloutAnnotation = (CalloutAnnotation *)annotation;
    ((CalloutAnnotationView *)annotationView).title = calloutAnnotation.title;
    [annotationView setNeedsDisplay];
    // Move the display position of MapView.
    [UIView animateWithDuration:0.5f
    animations:^(void) {
      mapView.centerCoordinate = calloutAnnotation.coordinate;
    }];
  }
  annotationView.annotation = annotation;
  return annotationView;
}

これで下記の用にカスタムコールアウトが表示されたハズです。

つまり、吹出しに表示したいタイトルをPinAnnotation > CalloutAnnotation > CalloutAnnotationView 経由で渡して吹出しをMKAnnotationViewとして表示するということです。(ここら辺がもっとスマートにならないかな。。。)

で、最後にPinAnnotationの選択が解除されたときに吹出しを消したいので「- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view」で地図のremoveAnnotationでCalloutAnnotationを削除します。

– (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
  if ([view.annotation isKindOfClass:[PinAnnotation class]]) {
    // Deselected the pin annotation.
    PinAnnotation *pinAnnotation = ((PinAnnotation *)view.annotation);
    [mapView removeAnnotation:pinAnnotation.calloutAnnotation];
    pinAnnotation.calloutAnnotation = nil;
  }
}

サンプルソースはgithubの下記にアップしておきます。

tochi / custom-callout-sample-for-iOS

なんかもっといい方法があるよ!とかこのコードだとココマズイんじゃね?ってところがありましたら教えてください。