【Swift】detailCalloutAccessoryViewの使い方。ピンの吹き出しをカスタマイズする。(Swift 2.1、XCode 7.2)
吹き出しのカスタマイズ
過去の記事でさりげなく使っていたが、ピンのタイトルの左右に部品(以下、吹き出しアクセサリー)を追加することができる。
タイトルの横に表示され、吹き出しのサイズを変更できない部品ため、デザインの自由度が今ひとつだった。そのため、地図に図形を描画して自作吹き出しを行う強者もいたようだ。
そんな中、iOS9でdetailCalloutAccessoryViewプロパティ(以下、詳細吹き出しアクセサリー)が登場し、吹き出しのサイズやレイアウトを簡単にカスタマイズできるようになった。
そこで本記事では、左右の吹き出しアクセサリーと、iOS9で登場した詳細吹き出しアクセサリーの使い方を説明する。
以降の手順は「Map Kit Viewの使い方」の続きから行うので、実装を試してみる人は先に読んでおくことをお勧めする。
左右の吹き出しアクセサリーを実装する
地図を長押しした座標にピンを刺し、吹き出しのタイトルの左右にボタンを表示する。左のボタンを押すとピンの色が変わり、右のボタンを押すとピンを削除するものを作ってみよう。
デバイス画面にLong Press Gesture Recognizerを配置する(下図赤矢印)。紫枠のアシスタントエディタボタンを押してViewController.swiftを開く。Ctrlキーを押しながら黄緑枠のジェスチャーリコグナイザーをドラッグ&ドロップでソースコードまで運んで吹き出しの設定画面を表示させる。
Connectionに「Action」、Nameに「pressMap」、Typeに「UILongPressGestureRecognizer」を設定しConnectボタンを押す。これで地図を長押ししたときのイベントをソースコードで受けれるようになった。
UIViewController.swiftを以下のコードに変更する。
デリゲートメソッドの「アノテーションビューを返すメソッド」で左右の吹き出しアクセサリーをピンに追加している。
もう1つのデリゲートメソッド「吹き出しアクセサリー押下時の呼び出しメソッド」は左右どちらのボタンが押されても呼び出されるので、引数で渡ってきたcontrolと左の吹き出しアクセサリーが一致するかどうかで動作を場合分けしている。
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
// // ViewController.swift // import UIKit import MapKit class ViewController: UIViewController,MKMapViewDelegate { @IBOutlet weak var testMapView: MKMapView! //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //中心座標 let center = CLLocationCoordinate2DMake(35.0, 140.0) //表示範囲 let span = MKCoordinateSpanMake(1.0, 1.0) //中心座標と表示範囲をマップに登録する。 let region = MKCoordinateRegionMake(center, span) testMapView.setRegion(region, animated:true) //デリゲート先に自分を設定する。 testMapView.delegate = self } //マップビュー長押し時の呼び出しメソッド @IBAction func pressMap(sender: UILongPressGestureRecognizer) { //マップビュー内のタップした位置を取得する。 let location:CGPoint = sender.locationInView(testMapView) if (sender.state == UIGestureRecognizerState.Ended){ //タップした位置を緯度、経度の座標に変換する。 let mapPoint:CLLocationCoordinate2D = testMapView.convertPoint(location, toCoordinateFromView: testMapView) //ピンを作成してマップビューに登録する。 let annotation = MKPointAnnotation() annotation.coordinate = CLLocationCoordinate2DMake(mapPoint.latitude, mapPoint.longitude) annotation.title = "タイトル" annotation.subtitle = "サブタイトル。長い文章を入れると表示が見切れてしまいます。" testMapView.addAnnotation(annotation) } } //アノテーションビューを返すメソッド func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? { //アノテーションビューを作成する。 let pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil) //吹き出しを表示可能にする。 pinView.canShowCallout = true //左ボタンをアノテーションビューに追加する。 let button = UIButton() button.frame = CGRectMake(0,0,40,40) button.setTitle("色", forState: .Normal) button.setTitleColor(UIColor.blackColor(), forState:.Normal) button.backgroundColor = UIColor.yellowColor() pinView.leftCalloutAccessoryView = button //右ボタンをアノテーションビューに追加する。 let button2 = UIButton() button2.frame = CGRectMake(0,0,40,40) button2.setTitle("削除", forState: .Normal) button2.backgroundColor = UIColor.redColor() button2.setTitleColor(UIColor.whiteColor(), forState:.Normal) pinView.rightCalloutAccessoryView = button2 return pinView } //吹き出しアクササリー押下時の呼び出しメソッド func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { if(control == view.leftCalloutAccessoryView) { //左のボタンが押された場合はピンの色をランダムに変更する。 if let pinView = view as? MKPinAnnotationView { pinView.pinTintColor = UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0) } } else { //右のボタンが押された場合はピンを消す。 mapView.removeAnnotation(view.annotation!) } } } |
以下は実際のプレイ動画。
ちなみにボタンのサイズを100×100ピクセルにした場合は下図のようになる。ボタンが大きくなっても吹き出しの高さは大きくならない。
詳細吹き出しアクセサリーを実装する。
次にiOS9で登場した詳細吹き出しアクセサリーを試してみよう。
下図のように、店舗情報と詳細ボタンを縦に並べる吹き出しを実装する。部品を縦に並べるためにVirtical Stack View(以下、垂直スタックビュー)を利用する。⇒「Virtical Stack Viewとは」
ピンを刺した場所に店、サービスがあるかはGoogle Places APIのプレイス検索で確認する。
以降の手順は上記の手順の続きになるが、その前に公式ガイドの手順に従ってGoogle Places APIを利用するための準備をする必要がある。⇒「公式ガイド」
準備でハマリやすそうな点は次の記事にまとめてあるので参照されたし。⇒「ハマりやすそうな点」
準備が終わったらUIViewController.swiftを以下のコードに変更する。
ピンを刺す座標の半径5m以内に店やサービスがあるかをGoogle Places APIのプレイス検索で確認し、ある場合はプレイス詳細とプレイスフォトで店舗の詳細情報と写真を取得し、垂直スタックビューに追加している。
写真の取得に時間がかかるので、詳細吹き出しアクセサリーの設定が終わり次第ピンの色を青に変更して読み込み完了を知らせるようにした。半径5m以内に店、サービスが無い場合はピンは赤のままである。
「詳細を見る」ボタンを押したときの処理は実装していない。ちなみに、詳細吹き出しアクセサリーを使うとピンのサブタイトルは表示されなくなる。
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
// // ViewController.swift // import UIKit import MapKit import GoogleMaps class ViewController: UIViewController,MKMapViewDelegate{ @IBOutlet weak var testMapView: MKMapView! let KEY = "AIzaSyCmRUhPeAflUcvRBgbueT8sFJCUPuNAnj0" //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //デリゲート先を自分に設定する。 testMapView.delegate = self //中心座標 let center = CLLocationCoordinate2DMake(35.690553, 139.699579) //表示範囲 let span = MKCoordinateSpanMake(0.001, 0.001) //中心座標と表示範囲をマップに登録する。 let region = MKCoordinateRegionMake(center, span) testMapView.setRegion(region, animated:true) } //マップビュー長押し時の呼び出しメソッド @IBAction func pressMap(sender: UILongPressGestureRecognizer) { //マップビュー内のタップした位置を取得する。 let location:CGPoint = sender.locationInView(testMapView) if (sender.state == UIGestureRecognizerState.Ended){ //タップした位置を緯度、経度の座標に変換する。 let mapPoint:CLLocationCoordinate2D = testMapView.convertPoint(location, toCoordinateFromView: testMapView) //ピンを作成してマップビューに追加する。 let annotation = MKPointAnnotation() annotation.coordinate = CLLocationCoordinate2DMake(mapPoint.latitude, mapPoint.longitude) testMapView.addAnnotation(annotation) } } //アノテーションビューを返すメソッド func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? { //アノテーションビューを作成する。 let pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier:nil) //検索URLを作成する。 let url = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(annotation.coordinate.latitude),\(annotation.coordinate.longitude)&types=establishment&radius=5&key=\(KEY)" let testNSURL = NSURL(string: url) //検索を実行する。 let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) session.dataTaskWithURL(testNSURL!, completionHandler: { (data : NSData?, response : NSURLResponse?, error : NSError?) in if error != nil { print("検索でエラー発生\(error)") } else { if let statusCode = response as? NSHTTPURLResponse { if statusCode.statusCode != 200 { print("サーバーから期待するレスポインスが来ませんでした。\(response)") } } do { //レスポンスデータ(JSON)から辞書を作成する。 let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary let results = json["results"] as? Array<NSDictionary> //検索結果の件数ぶんループ for result in results! { //吹き出しに表示するスタックビューを生成する。 let stackView = UIStackView() stackView.axis = UILayoutConstraintAxis.Vertical stackView.alignment = UIStackViewAlignment.Leading //画像があるか確認する。 if let aaa = result["photos"] as? NSArray { if let bbb = aaa[0] as? NSDictionary { if let photo_ref = bbb["photo_reference"] as? String { //フォトプレイスに要求するURL let url = NSURL(string: "https://maps.googleapis.com/maps/api/place/photo?maxheight=100&maxwidth=300&photoreference=\(photo_ref)&key=\(self.KEY)") //画像をスタックビューに追加する。 let imageData:NSData = try NSData(contentsOfURL: url!, options: NSDataReadingOptions.DataReadingMappedIfSafe) let imageView = UIImageView(image:UIImage(data:imageData)) stackView.addArrangedSubview(imageView) } } } //placeIDを取得する。 let placeID = result["place_id"] as? String //placeIDで店の詳細情報を取得する。 let placesClient = GMSPlacesClient() placesClient.lookUpPlaceID(placeID!, callback: { (place: GMSPlace?, error: NSError?) -> Void in if let error = error { print("プレイス詳細要求でエラー発生 \(error.localizedDescription)") return } if let place = place { //吹き出しのタイトルに店名を設定する。 if let pointAnnotation = annotation as? MKPointAnnotation { pointAnnotation.title = place.name } //スタックビューに住所を追加する。 if let text = place.formattedAddress { let testLabel2:UILabel = UILabel() testLabel2.frame = CGRectMake(0,0,200,0) testLabel2.sizeToFit() testLabel2.text = text stackView.addArrangedSubview(testLabel2) } //スタックビューに電話番号を追加する。 if let text = place.phoneNumber { let testLabel:UILabel = UILabel() testLabel.frame = CGRectMake(0,0,200,0) testLabel.text = text stackView.addArrangedSubview(testLabel) } //スタックビューにサイトURLを追加する。 if let text = place.website { let testLabel3:UILabel = UILabel() testLabel3.frame = CGRectMake(0,0,200,0) testLabel3.sizeToFit() testLabel3.text = String(text) stackView.addArrangedSubview(testLabel3) } //スタックビューにボタンを追加する。 let button = UIButton() button.frame = CGRectMake(0,0,200,50) button.backgroundColor = UIColor.blueColor() button.setTitleColor(UIColor.whiteColor(), forState:.Normal) button.setTitle("詳細を見る", forState:.Normal) stackView.addArrangedSubview(button) } else { print("詳細情報を取得できませんでした \(placeID)") } }) //ピンの吹き出しにスタックビューを設定する。 pinView.detailCalloutAccessoryView = stackView //吹き出しの表示をONにする。 pinView.canShowCallout = true //ピンの色を黄色にする。 pinView.pinTintColor = UIColor.blueColor() break } } catch { print("エラー") } } }).resume() return pinView } } |
以下は実際のプレイ動画。写真の読み込み完了までが遅いため、ピンが青くなるまでに時間がかかるのが気になるが、店の写真を吹き出しに大きく表示できるようになったのは嬉しい。