1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

経緯

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

手順

  1. こちらの記事を参考にしながら、とりあえず現在地をアプリ上に表示してください。

  1. ここからが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以外に色々あるみたいです。こちらの公式ドキュメントを参考にしてください。

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

    パラミーター名=パラミーターの中身&パラミーター名=パラミーターの中身
    

    という風につなげてください。


  2. そしてこの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を代入しました。


  1. マーカーをカフェの数だけ作ります。マーカーの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
           
           
           
       }
       
    }
    

  2. 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)&center=\(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)&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
 Github

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?