【Swift】MKLocalSearchの使い方。店名やキーワードで検索してピンを刺す。(Swift 2.1、XCode 7.2)
店名やキーワードで目的地を探す
過去の記事で、住所を座標に変換してピンを刺す方法を説明した。⇒「記事」
住所から目的地を探す使い方の他に、店名やキーワードで周辺の目的地を探すことも多そうだ。そこで、本記事では店名やキーワードから周辺の目的地を検索してピンを刺す方法を説明する。
以降の手順は「Map Kit Viewの使い方」の続きから行うので、実装を試してみる人は先に読んでおくことをお勧めする。
MKLocalSearchを使って周辺検索をする
デバイス画面にSearch Bar(以下、検索バー)を配置する(下図赤矢印)。黄緑枠のアシスタントエディタボタンを押してViewController.swiftを開く。Ctrlキーを押しながら検索バーをドラッグ&ドロップでソースコードまで運んで吹き出しの設定画面を表示する。
Connectionに「Outlet」、Nameに「testSearchBar」を入力してConnectボタンを押す。これでソースコードから検索バーを操作できるようになった。
ViewController.swiftを以下のコードに変更する。
店名やキーワードから目的地を探すにはMKLocalSearchクラスを利用する。startWithCompletionHandlerメソッドを引数「検索条件」と「クロージャ」で呼び出すだけで周辺の建物を検索してくれる優れもの。「クロージャ」には変換結果とエラー情報の2つの変数が渡ってくるので、エラー情報に何も設定されていないときは検索成功としてピンを刺している。
クロージャにについて詳しくは次の記事を参照されたし。⇒「クロージャとは」
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 |
// // ViewController.swift // import UIKit import MapKit class ViewController: UIViewController, UISearchBarDelegate{ @IBOutlet weak var testSearchBar: UISearchBar! @IBOutlet weak var testMapView: MKMapView! var testManager:CLLocationManager = CLLocationManager() //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //中心座標 let center = CLLocationCoordinate2DMake(35.690553, 139.699579) //表示範囲 let span = MKCoordinateSpanMake(0.001, 0.001) //中心座標と表示範囲をマップに登録する。 let region = MKCoordinateRegionMake(center, span) testMapView.setRegion(region, animated:true) //デリゲート先を自分に設定する。 testSearchBar.delegate = self } //検索ボタン押下時の呼び出しメソッド func searchBarSearchButtonClicked(searchBar: UISearchBar) { //キーボードを閉じる。 testSearchBar.resignFirstResponder() //検索条件を作成する。 let request = MKLocalSearchRequest() request.naturalLanguageQuery = testSearchBar.text //検索範囲はマップビューと同じにする。 request.region = testMapView.region //ローカル検索を実行する。 let localSearch:MKLocalSearch = MKLocalSearch(request: request) localSearch.startWithCompletionHandler({(result, error) in for placemark in (result?.mapItems)! { if(error == nil) { //検索された場所にピンを刺す。 let annotation = MKPointAnnotation() annotation.coordinate = CLLocationCoordinate2DMake(placemark.placemark.coordinate.latitude, placemark.placemark.coordinate.longitude) annotation.title = placemark.placemark.name annotation.subtitle = placemark.placemark.title self.testMapView.addAnnotation(annotation) } else { //エラー print(error) } } }) } } |
以下は実際のプレイ動画
MKLocalSearchクラスを使ってみたところ、下図のように1回の検索で抽出されるデータは最大10件までで、毎回同じ検索結果になるとは限らない。また、検索範囲を狭くしても範囲外にあるデータも検索結果に入ってくる結果となった。
もしかすると検索結果を増やす術があるのかも知れないので、分かり次第追記する。何か知っている方は教えて頂けると幸いである。
Google Place APIを使って周辺検索をする
仮に上記のような仕様の場合、周辺検索としては物足りなさを感じてしまう。そこで、マップやピンはSwiftの部品を使い、周辺検索の部分だけGoogleが提供しているGoogle Places APIを利用して実装してみよう。
Google Places APIとは、Googleマップと同じデータベースにアプリからアクセスできるAPIである。現在では旅行や飲み会、デートまで、どんなときでもGoogleマップが利用されるマップの頂点に立つ存在だ。
利用するには下準備が必要となる。公式ガイドに手順が載っているので参照されたし。⇒「公式ガイド」
準備でハマりそうな点を以下にメモしておく(公式ガイドは2016年5月現在のものを使用)。
ハマりそうな点1
ステップ3の「pod install」を実行するときに、「プロジェクト名.xcodeproj」のファイルが「PodFile」と同じ場所に無いと以下のエラーが発生する。
[!] Unable to find the Xcode project /Users/test/Documents/testPlayGround/UseLocation4/.xcodeproj.xcodeproj
for the target Pods
.
以下のように「PodFile」にプロジェクトファイルへのパスを記述すればエラーは解消する。
1 2 3 4 5 |
source 'https://github.com/CocoaPods/Specs.git' xcodeproj '../プロジェクト名' platform :ios, '9.2' pod 'GoogleMaps' |
ハマりそうな点2
準備が終わったらXCodeを再起動する必要がある。その際、「プロジェクト名.xcodeproj」では無く「プロジェクト名.xcworkspace」をダブルクリックして起動すること。
ハマりそうな点3
ステップ4で「Google Places API for iOS」 と 「Google Maps SDK for iOS」を有効にすると記載されているが、筆者のシミュレーターを使ったテスト環境では「Google Places API Web Service」も有効にしないと以下のエラーが発生して検索エラーが発生する。
“error_message" = “This API project is not authorized to use this API. Please ensure that this API is activated in the APIs Console: https://console.developers.google.com/apis/library?project=_ .."
ハマりそうな点4
ステップ5で[iOS key] を作るように記載されているが、筆者のシミュレーターを使った環境ではiOSキーを使うと以下のエラーが発生して検索に失敗し、「サーバーキー」を使うとエラーは解消する。次のページが参考になった。⇒「This IP, site or mobile…」
ハマりそうな点5
AppDelegate.swiftで取得したキーをGMSServicesに設定するのを忘れないこと。
GMSServices.provideAPIKey(“取得したキー")
ハマりそうな点は以上。
次に、ViewController.swiftを以下のコードに変更する。
検索ボタンが押された段階で検索URLを作成してGoogleに問い合わせを行っている。GoogleからはJSON形式のデータが検索結果として返ってくるので、データの件数ぶんのピンを作成する。
検索結果は最大60件のデータが1回20件に分割されて返ってくる。検索結果のnext_page_tokenに値が設定されているときは続きがあることを意味しているので、それをもとにGoogleに再問い合わせを行う作りにしている。
また、Google側で検索結果の切り替えに若干の時間を要するため、時間を空けずに連続で問い合わせると検索結果無しが返ってきてしまう。なので、1秒のスリープを入れてから再問い合わせを行っている。
検索処理はメインスレッドの処理とは非同期で行われる。そのため、検索処理の中でマップビューにピンを追加すると、画面を指でタッチしないとピンが描画されないという事象が発生した。そこで、セマフォを使って検索処理とメインスレッドの処理を同期させ、検索処理がすべて終わった段階でマップビューにピンを追加する仕様にした。
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 |
// // ViewController.swift // import UIKit import MapKit class ViewController: UIViewController, UISearchBarDelegate{ @IBOutlet weak var testSearchBar: UISearchBar! @IBOutlet weak var testMapView: MKMapView! var testManager:CLLocationManager = CLLocationManager() //中心座標 let center = CLLocationCoordinate2DMake(35.690553, 139.699579) let KEY = "取得したキー" //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //表示範囲 let span = MKCoordinateSpanMake(0.01, 0.01) //中心座標と表示範囲をマップに登録する。 let region = MKCoordinateRegionMake(center, span) testMapView.setRegion(region, animated:true) //デリゲート先を自分に設定する。 testSearchBar.delegate = self } //検索ボタン押下時の呼び出しメソッド func searchBarSearchButtonClicked(searchBar: UISearchBar) { var annotationList = [MKPointAnnotation]() var page_token:String = "" //検索1回に最大20件の検索結果が返ってくるので、検索結果が20件以上ある場合は複数回要求をかける。 repeat { //セマフォを使って、検索とメインスレッドを同期で処理する。 let semaphore = dispatch_semaphore_create(0) //検索URLを作成する。 let encodedStr = testSearchBar.text!.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) let url = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(center.latitude),\(center.longitude)&radius=2000&sensor=true&key=\(KEY)&name=\(encodedStr!)&pagetoken=\(page_token)" 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> //次のページがあるか確認する。 if json["next_page_token"] != nil { page_token = json["next_page_token"] as! String } else { page_token = "" } //検索結果の件数ぶんループ for result in results! { let annotation = MKPointAnnotation() //ピンのタイトルに店名、住所を設定する。 annotation.title = result["name"] as? String annotation.subtitle = result["vicinity"] as? String if let geometry = result["geometry"] as? NSDictionary { if let location = geometry["location"] as? NSDictionary { //ビンの座標を設定する。 annotation.coordinate = CLLocationCoordinate2DMake(location["lat"] as! CLLocationDegrees, location["lng"] as! CLLocationDegrees) annotationList.append(annotation) } } } } catch { print("エラー") } } //連続で要求をすると結果が返ってこないので一瞬スリープする。 sleep(1) //処理終了をセマフォに知らせる。 dispatch_semaphore_signal(semaphore) }).resume() //検索が終わるのを待つ。 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) } while (page_token != "") //キーボードを閉じる。 testSearchBar.resignFirstResponder() //ピンをマップに追加する。 testMapView.addAnnotations(annotationList) } } |
以下は実際のプレイ動画。60本のピンが刺さった。