経緯
2・3年前に作ったアプリでGoogle Map上で近くのカフェをマーカーに映し出し、そのカフェの詳細を知るためにGoogle Mapsのアプリに飛ぶという機能が使えなくなっていたためリファクターしました。
手順
- こちらの記事を参考にしながら、とりあえず現在地をアプリ上に表示してください。
-
ここからがPlace APIの出番です。私の場合近くのカフェを知りたいのでカフェを検索する手順を紹介します。
let session = URLSession.shared let locationManager = CLLocationManager() guard let url = URL(string: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(locationManager.location!.coordinate.latitude),\(locationManager.location!.coordinate.longitude)&radius=4500&type=cafe&key=\(String(describing: apiKey))") else { return }
urlのところを見て色々書いてあるので解説します。まずは下記がURLの構造になっています。
https://maps.googleapis.com/maps/api/place/nearbysearch/output?parameters
outputはAPI通信でリクエストを投げて結果が返ってくるとき、なんの形で形で返って来て欲しいかを指定します。私の場合jsonです。XMLも指定できるみたいです。
パラミーターの種類はこちらの公式ドキュメントの中のRequired Parameters, Optional Parametersを参考にしてください。パラミーター"type"は場所の種類を指定するものです。cafe以外に色々あるみたいです。こちらの公式ドキュメントを参考にしてください。
そしてパラミーターのところは
パラミーター名=パラミーターの中身&パラミーター名=パラミーターの中身
という風につなげてください。
-
そしてこのURLにリクエストを投げてAPI通信を行います。今回はURLSessionを使ってAPI通信を行い、デコードしております。
let task = session.dataTask(with: url) { data, response, error in if let error = error { print(error) if data == nil { print("Client error!") return } } else { guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { print("Server error!") return } guard let mime = response.mimeType, mime == "application/json" else { print("Wrong MIME type!") return } do { let json = try JSONSerialization.jsonObject(with: data!, options: []) print(json) } catch { print("JSON error: \(error.localizedDescription)") } do { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let root = try decoder.decode(Root.self, from: data!) print(root) self.root = root self.createMarkers() } catch (let err) { print(err) } } }
struct Root { let results: [SearchResult] let status: String } extension Root: Decodable { enum CodingKeys: String, CodingKey { case results, status } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.results = try values.decode([SearchResult].self, forKey: .results) self.status = try values.decode(String.self, forKey: .status) } } struct SearchResult { let icon: String let name: String let placeId: String let reference: String let types: [String] let vicinity: String let geometry: Geometry let photos: [Photo] let openingHours: [String:Bool]? } extension SearchResult: Decodable { enum CodingKeys: String, CodingKey { case placeId, name, vicinity, reference, icon, geometry, photos, types, openingHours = "openingHours" } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.placeId = try values.decode(String.self, forKey: .placeId) self.name = try values.decode(String.self, forKey: .name) self.vicinity = try values.decode(String.self, forKey: .vicinity) self.reference = try values.decode(String.self, forKey: .reference) self.icon = try values.decode(String.self, forKey: .icon) self.geometry = try values.decode(Geometry.self, forKey: .geometry) do { self.photos = try values.decode([Photo].self, forKey: .photos) } catch(let err) { print(err) self.photos = [] } self.types = try values.decode([String].self, forKey: .types) do { self.openingHours = try values.decode([String: Bool]?.self, forKey: .openingHours) } catch (let err) { print(err) self.openingHours = nil } } }
ここで私がハマったところを2点紹介します。
-
Coding keysのrawValueはキャメルケースにしてください。私はjsonのfiled名と同じにしなきゃいけないと思ってました...
enum CodingKeys: String, CodingKey { case placeId, name, vicinity, reference, icon, geometry, photos, types, openingHours = "openingHours" // not opening_hours }
-
返ってくるjson, xmlに一部のFieldが時々ないことがあるので、そのFieldの値をデコードする際は個別でdo-catchしてエラーが出てきたらnilを代入しました。
-
-
マーカーをカフェの数だけ作ります。マーカーのtitleに場所の名前、snipetにvacinityを代入します。こちらは後ほど使います。
func createMarkers() { root.results.forEach { place in let marker = GMSMarker() marker.position = CLLocationCoordinate2D(latitude: place.geometry.location.lat , longitude: place.geometry.location.lng) marker.title = "\(place.name)" marker.snippet = "\(place.vicinity)" marker.map = mapView } }
-
mapを表示するViewControllerに CLLocationManager().delegate = selfをどこかに記述し、GMSMapViewDelegateを拡張したExtension内でfunc mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker)が使えます。その中でタップしたマーカーの場所に飛ぶURLを作り、実際に飛ぶ感じです。
func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { guard let title = marker.title, let address = marker.snippet else { return } //タップされたマーカーのplace_idを取得できればこちらはどんな方法でも構いません。 guard let placeId = self.root.results.first(where: { $0.vicinity == address })?.placeId else { return } let urlString = "https://www.google.com/maps/search/?api=1&query=\(title)&query_place_id=\(placeId)¢er=\(marker.position.latitude),\(marker.position.longitude)" //注目! if let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedUrlString) { UIApplication.shared.open(url) } }
-
公式ドキュメントに記載されていますが、パラメーター"query"は必須で、"query_place_id"は必須ではありません。
ただ"query_place_id"を指定するとこちらを優先して、見つかった場合"query"の値を使わず、その場所を探すので指定した方がいいかもしれません。
let urlString = "https://www.google.com/maps/search/?api=1&query=\(title)&query_place_id=\(placeId)¢er=\(marker.position.latitude),\(marker.position.longitude)"
-
addressが日本語のため、URLをエンコードしないといけないです。完全にハマりました。URLに日本語みたいな言語が入っていたら、エンコードした方が良さそうですね。
if let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedUrlString) {
-
最期に
iOSエンジニアとして就活中です!3年ほどiOS開発について勉強しています。
そしてインターンでWordPressを使いながら実務でHTML・CSSを扱っています。Reactも勉強中です。
ポートフォリオでは以下を使用しています。
- Firebase
- RxSwift
- CoreData
- Alamofire, URLSession
- OAuthを用いた認証
- MVC, MVVM, VIPER
- UIKit
- Kingfisher