Google Maps API for iOSとPlaces APIと Maps URLsでマーカーからGoogle Mapに飛ぶ


## 経緯

　2・3年前に作ったアプリでGoogle Map上で近くのカフェをマーカーに映し出し、そのカフェの詳細を知るためにGoogle Mapsのアプリに飛ぶという機能が使えなくなっていたためリファクターしました。



## 手順

1. [こちら](https://qiita.com/nwatabou/items/38f4240582d70a4d84a8)の記事を参考にしながら、とりあえず現在地をアプリ上に表示してください。

<br>

2. ここからがPlace APIの出番です。私の場合近くのカフェを知りたいのでカフェを検索する手順を紹介します。

    <br>
   
   ```Swift
        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も指定できるみたいです。
　  
    パラミーターの種類は[こちらの公式ドキュメント](https://maps.googleapis.com/maps/api/place/nearbysearch/output?parameters)の中のRequired Parameters, Optional Parametersを参考にしてください。
 
    パラミーター"type"は場所の種類を指定するものです。cafe以外に色々あるみたいです。[こちらの公式ドキュメント](https://developers.google.com/maps/documentation/places/web-service/supported_types)を参考にしてください。

   そしてパラミーターのところは

   ```
   パラミーター名=パラミーターの中身＆パラミーター名=パラミーターの中身
   ```
   
   という風につなげてください。

   <br>

3. そしてこのURLにリクエストを投げてAPI通信を行います。今回はURLSessionを使ってAPI通信を行い、デコードしております。

    ```Swift
    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)
                    
                }
                
            }
        }
        

    ```

    ```Swift
        
        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名と同じにしなきゃいけないと思ってました...
  
        ```Swift
            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を代入しました。
  
  <br>

4. マーカーをカフェの数だけ作ります。マーカーのtitleに場所の名前、snipetにvacinityを代入します。こちらは後ほど使います。

     ```Swift
        
        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
            
            
            
        }
        
    }
    ```
    <br>

5. mapを表示するViewControllerに CLLocationManager().delegate = selfをどこかに記述し、GMSMapViewDelegateを拡張したExtension内でfunc mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker)が使えます。その中でタップしたマーカーの場所に飛ぶURLを作り、実際に飛ぶ感じです。

    ```Swift
     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)&center=\(marker.position.latitude),\(marker.position.longitude)"
        
        
        //注目！

        if let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedUrlString) {
            
            UIApplication.shared.open(url)
            
        }
        
    }

    ```

    - [公式ドキュメント](https://developers.google.com/maps/documentation/urls/get-started)に記載されていますが、パラメーター"query"は必須で、"query_place_id"は必須ではありません。
    ただ"query_place_id"を指定するとこちらを優先して、見つかった場合"query"の値を使わず、その場所を探すので指定した方がいいかもしれません。
       
        <br>

        ```Swift
        
        let urlString = "https://www.google.com/maps/search/?api=1&query=\(title)&query_place_id=\(placeId)&center=\(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も勉強中です。

　[Twitter](https://twitter.com/Shihomivancouv1)
　[Github](https://github.com/shihominorth?tab=repositories)

　ポートフォリオでは以下を使用しています。

- Firebase
- RxSwift
- CoreData
- Alamofire, URLSession
- OAuthを用いた認証
- MVC, MVVM, VIPER
- UIKit
- Kingfisher