くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Google Mapを使ってみよう

今回はGoogle Mapをモバイル開発で使う流れをまとめようと思います。 AndroidiOS両方説明しようと思います。
AndroidiOSでできることにはほとんど差がないのでコードの書き方が少し違うだけだと思います。

iOSGoogle Mapの対応を行う

最初にマップを表示させるところから始めましょう!

事前準備

まずは、マップを使えるようにするために次の手順を行う必要があります。(順不同)

  1. APIキーを取得してプロジェクトに導入
  2. ライブラリを使えるようにする
  3. 位置情報の権限追加

まずはAPIキーを取得します。 どのアプリケーションからGoogle Mapを使うかを判定、リクエストを行うためにAPIキーを取得してプロジェクトで設定する必要があります。

APIキーを取得は次の公式ドキュメントを確認してください。 対象のアプリでGoogle Mapの使用するかでONを設定すると認証情報の欄でAPI Keyが確認できます。

developers.google.com

そして、AppDelegateで下記のようにAPIキーを設定します。

GMSServices.provideAPIKey("取得したAPIキーを設定")

次にライブラリを使えるようにpodファイルにGoogle Mapのライブラリを追加してpod installを行います。

pod 'GoogleMaps'

そして、位置情報の権限追加はinfo.plistに位置情報の権限を追加します。 細かい記載は今回割愛します。

マップを表示

事前準備を行ったら実際にマップを表示させます。

class MapViewController: UIViewController {
    private var locationManager: CLLocationManager?
    private var mapView: GMSMapView?

    deinit {
        if let manager = locationManager {
            manager.stopUpdatingLocation()
            locationManager = nil
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setupLocationManager()
    }

    private func setupLocationManager() {
        let locationManager = CLLocationManager()
        locationManager.delegate = self
        // 位置情報の取得精度を指定
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

        // 位置情報の権限確認
        let status = CLLocationManager.authorizationStatus()

        switch status {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            setupGoogleMaps()
        case .authorizedAlways, .authorizedWhenInUse:
            locationManager.startUpdatingLocation()
        @unknown default:
            setupGoogleMaps()
        }

        self.locationManager = locationManager
    }
}

extension MapViewController {
    private func setupGoogleMaps() {
        if mapView != nil {
            return
        }

        var location: CLLocation!

        if let lastKnownLocation = locationManager?.location {
            location = lastKnownLocation
        } else {
            location = CLLocation(latitude: 0, longitude: 0)
        }

        // 現在地とマップがどれくらいズームするかを設定する
        let camera = GMSCameraPosition.camera(
            withLatitude: location.coordinate.latitude,
            longitude: location.coordinate.longitude,
            zoom: 12.5
        )

        // GMSMapViewを初期化する
        let mapView = GMSMapView.map(
            withFrame: CGRect(x: 0, y: 0, width: view.frame.width,height: view.frame.height),
            camera: camera
        )

        // 現在値を中心にするボタンを有効にする設定
        mapView.settings.myLocationButton = true

        // AutoLayoutで定義したUIViewにaddSubViewする
        mapBaseView.addSubview(mapView)
        mapBaseView.sendSubviewToBack(mapView)

        self.mapView = mapView
    }
}

// MARK: - CLLocationManagerDelegate

extension MapViewController: CLLocationManagerDelegate {
    // 権限が変更されたときの処理
    func locationManager(_ manager: CLLocationManager, 
                           didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            manager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            setupGoogleMaps()
        case .authorizedAlways, .authorizedWhenInUse:
            manager.startUpdatingLocation()
        @unknown default:
            setupGoogleMaps()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        manager.stopUpdatingLocation()
        setupGoogleMaps()
    }
}

上記は表示させるためのコードです。

setupLocationManager()で位置情報を許諾しているかの確認を行い、まだ1度も設定していない場合には確認のダイアログを表示させます。 許可している場合にはstartUpdatingLocation()で現在地を取得してマップに適用させます。
移動する場合にもCLLocationManagerDelegatefunc locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])で現在地を定期的に更新できます。

また、マップにはGMSMapViewクラスを使います。 GMSMapViewは、地図に関するすべてのメソッドの入り口で、表示するMapのクラスです。
setupGoogleMaps()でGoogleMapの表示設定を行いました。 大まかには初期表示の位置やどれくらいズームするかを設定したGMSMapViewを初期化して、AutoLayoutで定義したUIViewにaddSubViewすると表示できます。
それが上記のコードになります。

ピンを表示

次に特定の位置にピン(アイコン)を表示させる方法です。
ピンにはGMSMarkerクラスを使います。 先ほどの例のsetupGoogleMaps()の後にcreateMarker()メソッドを追加します。

class MapViewController: UIViewController {
    private var locationManager: CLLocationManager?
    private var mapView: GMSMapView?
}

extension MapViewController {
    private func setupGoogleMaps() {
        // 先ほどの例の部分は割愛

        createMarker()
    }

    private func createMarker() {
        let marker = GMSMarker()

        //ピンの色変更したい場合に設定
        marker.icon = GMSMarker.markerImage(with: .black) 
        marker.position = CLLocationCoordinate2D(latitude: 34.077875549971,
                                                 longitude: 134.56156512254)
        marker.title = "タイトル"
        //ピンの画像を変更したい場合に設定
        marker.icon = UIImage(named: "camera_1")!
        marker.zIndex = 2
        marker.map = mapView

        mapView.delegate = self
    }
}

extension MapViewController: GMSMapViewDelegate {
    // ピンをタップした時の処理
    func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
        return true
    }

    // マップをタップした時の処理
    func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {
    }

    func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
    }
}

今回はposition(表示させる緯度・経度の情報)と titleを含むGMSMarkerオブジェクトを作成し、GMSMapViewに設定します。 titleはピンをタップすると情報ウィンドウに設定した文字列が表示されます。 iconはアイコンの情報を設定することができ、色や画像の変更を行えます。
GMSMapViewDelegateを設定しているのはアイコンやマップをタップしたときに処理を行う場合には必要です。
また、zIndexを指定していますが、これはピンが重なった時にzIndexの値が大きい方が前に表示されます。
意外と簡単だと思った方が多いと思いますが、ここまでで最低限のマップアプリは作れると思います。

AndroidGoogle Mapの対応を行う

AndroidiOSと同様に進めます。
iOSと大きな違いがあるわけではないのでそこまで難しいことはないと思います。

マップを表示

まずはiOSと同様にマップを表示させるところから始めましょう!

最初にAPI Keyを設定します。
AndroidManifest.xmlAPI Keyの設定を追加します。

<meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="取得したAPI Keyを設定" />

そして、ライブラリをbuild.gradleに追加します。

implementation 'com.google.gms:google-services:4.3.14'
implementation 'com.google.android.gms:play-services-location:21.0.1'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1'

Mapを表示させるにはGoogleMapクラスを使用します。 LocationServices.getFusedLocationProviderClientで位置情報を取得します。 Mapを表示させるための一例は次のコードです。

class MapFragment : Fragment(), OnMapReadyCallback {
    private var mapView: GoogleMap? = null
    private var fusedLocationClient: FusedLocationProviderClient? = null

    private var _binding: FragmentMapBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentMapBinding.inflate(inflater, container, false)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        setupData()
    }

    override fun onResume() {
        super.onResume()
    }

    private fun setupData() {
        checkPermission()
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())

        val mapFragment =
            childFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment
        mapFragment.getMapAsync(this)     
    }

    override fun onMapReady(p0: GoogleMap) {
        this.mapView = p0
        fetchLocation()
    }

    private fun fetchLocation() {
        mapView?.uiSettings?.isMyLocationButtonEnabled = false
        if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_COARSE_LOCATION
            )
            != PackageManager.PERMISSION_GRANTED
        ) {
            return
        } else {
            mapView?.isMyLocationEnabled = true
            val task = fusedLocationClient?.lastLocation
            task?.addOnSuccessListener { location ->
                if (location != null) {
                    // Mapの中心の緯度・経度を設定して表示させる
                    mapView?.moveCamera(
                        CameraUpdateFactory.newLatLngZoom(
                            LatLng(location.latitude, location.longitude),
                            10.0f
                        )
                    )
                } else {
                    val request = LocationRequest.create()
                        .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                        .setInterval(500)
                        .setFastestInterval(300)

                    fusedLocationClient?.requestLocationUpdates(
                        request,
                        object : LocationCallback() {
                            override fun onLocationResult(result: LocationResult) {
                                mapView?.moveCamera(
                                    CameraUpdateFactory.newLatLngZoom(
                                        LatLng(
                                            result.lastLocation!!.latitude,
                                            result.lastLocation!!.longitude
                                        ),
                                        12.5f
                                    )
                                )
                                fusedLocationClient?.removeLocationUpdates(this)
                            }
                        }, Looper.getMainLooper()
                    )
                }
            }
        }
    }       

今回はFragmentでGoogle Mapを表示させていますが、Activityで表示させる場合にも基本的には同じです。 FragmentManager.findFragmentById() を呼び出して、地図フラグメントに対するハンドルを取得します。
次に、getMapAsync()を使用して、GoogleMapインスタンス生成完了時に呼ばれる地図コールバックを登録します。 OnMapReadyCallbackインターフェースを実装し、GoogleMap オブジェクトが使用可能な場合に地図を設定するようonMapReady()をオーバーライドします。

ピンを表示

次に特定の位置にピン(アイコン)を表示させる方法です。
iOSではピンにはGMSMarkerクラスを使いましたが、AndroidではMarkerクラスを使用します。 Mapが表示した後に下記の処理を行います。(onMapReady()や初期設定後)

private fun createOption(latLng: LatLng): MarkerOptions {
    val options = MarkerOptions().position(latLng)
    // アイコンを指定
    options.icon(BitmapDescriptorFactory.fromResource(R.drawable.pin))

    options.zIndex(2.0F)

    // mapViewはGoogle Mapクラスのインスタンス
    mapView?.addMarker(options)
}

上記の処理を行うと指定した緯度・経度にマーカーが表示されます。 削除する場合にはremove()を使用します。

ダークモード対応

作成したコードをダークモードで実行してもライトモードのままだと思います。 ダークモード対応は追加で別で実装が必要となるので、最後に補足します。

iOS

次のメソッドをマップの初期化や画面復帰した際の処理に追加してください。

private func setMapStyle(_ mapView: GMSMapView?) {
    guard let mapView = mapView else {
        return
    }

    if UITraitCollection.current.userInterfaceStyle == .dark {
        // ダークモードのjsonを読み込ませる
        if let styleURL = Bundle.main.url(forResource: "map_dark", withExtension: "json") {
            do {
                mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL)
            } catch {
                return
            }
        }
    } else {
        // ライトモードのjsonを読み込ませる
        if let styleURL = Bundle.main.url(forResource: "map_light", withExtension: "json") {
            do {
                mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL)
            } catch {
                return
            }
      }
}

何をやっているかというと、GMSMapViewのmapStylejsonで定義した色などの情報を適用させています。 背景色やラベルの色、道路の色など設定できます。
下記に色を設定してjsonファイルを作成してくれるサイトがあるので、作成は楽だと思います。

https://mapstyle.withgoogle.com

Android

AndroidiOSと同様にjsonで定義した色などの情報を適用させます。 他にも方法はあるかもしれませんが、iOSと実装方法を共通化できる点でjsonを使うのがおすすめです。

private fun setMapStyle(mapView: GoogleMap?) {
        mapView?.let { view ->
            val nightModeFlags = requireContext().resources.configuration.uiMode and
                Configuration.UI_MODE_NIGHT_MASK

            if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {
                view.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(
                        requireContext(), R.raw.map_dark
                    )
                )
            } else {
                view.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(
                        requireContext(), R.raw.map
                    )
                )
            }
        }
    }

最後に

両OSともにシンプルなコードでGoogle Mapを表示できたと思います。 もう少し突っ込んだことを実装したい場合には公式ドキュメントが細かく記載されているため確認してみるといいと思います。

参照

developers.google.com

qiita.com

developers.google.com

developers.google.com

akira-watson.com

qiita.com

qiita.com