こんにちは〜
見習いプログラマーのnaoponです!
みなさんは、iOSアプリで地図を使うとき何のマップを使っていますか? 僕はだいたい下記を使っています。
・Google の「GMSMapView」
・iOS の「MKMapView」
そして今回の『テーマ』はこちら↓↓↓
「iOSのコールアウトをカスタムしよう!」
目的は、
「Googleマップの吹き出し「infoWindow」と同じに、iOSマップの吹き出しも 好きなUIのコールアウトを出して使いたい」
■ カスタム手順の概要
1, InfoWindow作成
2, MKAnnotationのカスタム
3, MKPinAnnotationViewのカスタム
4, MKMapViewのカスタム
5, 実際に使ってみる
「んじゃ、どうやるの?」って話になりますよね。。
長々になってしまうので、簡潔に説明しますww
詳細は、ダウンロードしたサンプルを見てくださいね(^ ^;)
1, InfoWindowを作る
これは、各々UIViewをいつも通り作ってください。
サンプルでは、UIViewControllerベースにしてます。
1 2 3 4 5 6 |
//定義 @interface InfoWindow : UIViewController //吹き出しの枠線や画像、テキストなどUIはご自由にどうぞ @end |
2, MKAnnotationでアノテーションを作る
これも、各々いままで通りの作り方ですね。
好きなプロパティを定義して、色や画像や何やら好きにしちゃってください。
1 2 3 4 5 6 7 8 9 10 11 |
//定義 @interface IOSAnnotation : NSObject <MKAnnotation> @property (nonatomic, readonly) CLLocationCoordinate2D coordinate; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle; -(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate; -(void)setCoordinate:(CLLocationCoordinate2D)newCoordinate; @end |
3, MKPinAnnotationViewでコールアウトを作る
※ここは重要かも
「AnnotationView」(ピン)は、タップされると、内部で「CalloutView」(吹き出し)を自身に addSubView します。
1: ピンをタップ
2: ピンの中で [ -(void)didAddSubview:(UIView *)subview{} ] が呼ばれる
3: ピンの中で [ -(void)layoutSubviews{} ] が呼ばれる
だから、やることとして、
1: CalloutViewの参照を確保
2: CalloutView上の標準UIをクリア
3: CalloutView上にオリジナルUIをaddSubView
[注意点]
A: CalloutViewのclipsToBoundsをNOにすること
B: layoutSubviewsは何回も呼ばれるので気をつけること
これで、カスタム吹き出しはOKです。
下記のtmpCalloutViewにCalloutViewの参照を格納します。
あとで使うのでViewのFrameを返すメソッドも1つ定義しておいてください。
1 2 3 4 5 |
//定義 @interface IOSAnnotationView : MKPinAnnotationView @property (strong, nonatomic) UIView *tmpCalloutView; - (CGRect)infoWindowFrame; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//実装 - (void)didAddSubview:(UIView *)subview { if([NSStringFromClass([subview class]) isEqualToString:@"UICalloutView"] || [NSStringFromClass([subview class]) isEqualToString:@"UIView"]) { self.tmpCalloutView = subview; for(UIView *sv in [self.tmpCalloutView subviews]) { [sv removeFromSuperview]; } self.tmpCalloutView.backgroundColor = [UIColor clearColor]; self.tmpCalloutView.clipsToBounds = NO; //←ここ超重要☆ self.isLayouted = NO; } } |
※iOS7.0より前のバージョンでは「UICalloutView」、iOS7.0以上では「UIView」がコールアウトのViewとなります。
ちなみに「UICalloutView」をいじってリジェクトされないの?と不安になるかと思いますが、削除しているだけなので、リジェクトはされません。過去4度のApple申請でも、問題なくリリースすることができました!(ワ~イ)
上記コードの「ここ超重要☆」があるかと思いますが、なぜかというと、iOS7.0より前とiOS7.0以上では、コールアウトViewが持っているFrameサイズが極端に違います。
iOS7.0より前は、必要最低限のサイズで、iOS7.0以上は画面サイズがコールアウトViewの領域となり、オリジナルViewを追加した際に、領域からはみ出たレイアウトの部分が、カットされないようにします。self.tmpCalloutView.backgroundColorに好きな色をつけてみれば、地図上でコールアウトViewの領域がわかるかと思います。
次に、didAddSubviewのあとに、[-(void)layoutSubviews{}]が呼ばれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//実装 - (void)layoutSubviews { if(self.isLayouted == NO) { self.isLayouted = YES; //好きなレイアウト self.infoWindow = [[InfoWindow alloc] initWithNibName:@"InfoWindow" bundle:[NSBundle mainBundle]]; //吹き出しの中央位置 CGRect pinRect = [self convertRect:self.bounds toView:self.tmpCalloutView]; CGFloat px = CGRectGetMidX(pinRect); CGFloat py = CGRectGetMinY(pinRect) - [self division:CGRectGetHeight(self.infoWindow.view.bounds) by:2.0]; //ピンの画像によっては微調整 px = px - 8.0f; py = py - 3.0f; [self.infoWindow.view setCenter:CGPointMake(px, py)]; //InfoWindowに好きな情報をセットする self.infoWindow.messageLabel.text = self.annotation.title; //標準コールアウトにオリジナルUIを追加 [self asyncAfterDelay:0.0f block:^{ [self.tmpCalloutView addSubview:self.infoWindow.view]; }]; } } |
コールアウトのフレームサイズを返すメソッドも追加!
1 2 3 4 |
- (CGRect)infoWindowFrame { return [[self superview] convertRect:self.infoWindow.view.frame fromView:self.tmpCalloutView]; } |
4, ちょっとだけマップをカスタム
マップのタッチイベントを拾えるようにします。
そして、吹き出しをタップされた場合に、マップのデリゲートメソッドでコールアウトがタップされたことをViewControllerに流しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
//定義 @interface IOSMapView : MKMapView @property (strong, nonatomic) IOSAnnotationView *currentAnnotationView; @end @implementation IOSMapView - (BOOL)isCalloutHitPoint:(CGPoint)mapPoint { @autoreleasepool { if(self.currentAnnotationView == nil) { return NO; } else { //吹き出しのマップ位置を取得 CGRect infoRect = [self.currentAnnotationView.superview convertRect:[self.currentAnnotationView infoWindowFrame] toView:self]; //吹き出しタップ判定 if(CGRectContainsPoint(infoRect, mapPoint)) { return YES; } else { return NO; } } } } BOOL hasCalloutHit = NO; - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { @synchronized(self) { if(hasCalloutHit == NO) { hasCalloutHit = [self isCalloutHitPoint:point]; if(hasCalloutHit == YES) { [self.delegate mapView:self annotationView:self.currentAnnotationView calloutAccessoryControlTapped:nil]; [self asyncAfterDelay:0.1 block:^{ hasCalloutHit = NO; }]; return nil; } else { return [super hitTest:point withEvent:event]; } } else { return nil; } } } |
5, 使ってみる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#pragma mark - #pragma mark MKMapViewDelegate - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { if([view.annotation isKindOfClass:[IOSAnnotation class]]) { //選択中のアノテーションを保持 self.iosMapView.currentAnnotationView = (IOSAnnotationView *)view; } } - (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view { //選択解除されたアノテーションを破棄 self.iosMapView.currentAnnotationView = nil; } - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { if([view isKindOfClass:[IOSAnnotationView class]]) { NSLog(@"Info Window Tapped"); } } |
ふぅ〜〜〜
これでコールアウトのカスタムと実際に使うところまでが終了ですね。
以上『目標達成』ということで!
今は『コールアウトのタップイベント』をとっていますが、InfoWindowの中の各UIのFrameも判定に加えれば、『コールアウト内のどの項目がタップされたのか』もわかるかと思います。(Googleの方ができないのでiOSでそこまでは求めないかw)
サンプルはここからダウンロードできます。
みなさん参考になりましたか?
以上、見習いプログラマーnaoponでした〜!!