見出し画像

Apple Mapチュートリアル - 第2回:自分の位置を表示しよう

Apple Mapチュートリアルの第2回では、マップの操作の前に、ユーザーの現在位置を取得する方法を解説します。マップを表示するアプリのほとんどの場合でユーザーの現在位置を使うことが多いと思いますので、いきなり第2回でこの解説をすることにしました。

ユーザーの位置を取得するには、

1.ユーザーに位置情報を使っていいか聞く

2.ユーザーの許可が下りたら位置情報を取得する

という2段階の準備が必要なります。まずは、1番目のユーザーに位置情報を使っていいか聞く部分から用意していきましょう。

位置情報取得の許可を得る準備

その昔アプリがユーザーの位置情報を勝手に取得してもいい自由な時代がありました。。
しかし、今のモダンなiOSでは位置情報を取得するには下のようなダイアログを表示してユーザーの許可を得る必要があります。

スクリーンショット 2020-07-25 17.03.25

位置情報を取得するための実装はまずこのダイアログの2段目に表示される文言の設定から始まります。

まずプロジェクトのInfo.plistを開き、ここに文言の設定を追加します。

Info.plistの画面の下の空白の部分で右クリックをし、Add Rowを選んでください。

スクリーンショット 2020-07-25 11.26.29

Key名に、"Privacy"と入力すると"Privacy - Location When In Use Usage Description"というKeyが出てくるので、これを選択してください。

スクリーンショット 2020-07-25 11.28.04

Valueには、先程見せたダイアログの2段目に表示させたい説明文を入力してください。

スクリーンショット 2020-07-25 11.29.05

今回は、「このチュートリアルではあなたの位置情報を使用します。」としましたが、

作ろうとしているアプリが例えば写真の撮影場所を記録するアプリだった場合、「あなたが撮影した写真の撮影場所を記録するために位置情報を使用します。」のような文章を設定するとよりユーザーに目的が伝わっていいと思います。

コーディングの開始

それでは、ここからViewController.swiftに実装をしていきましょう。

まず、ViewControllerに下のCLLocationManagerクラスを宣言します。

var locationManager:CLLocationManager!

これをViewControllerのviewDidLoadで初期化してやります。

locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

このとき、delegateにselfを登録し、
desiredAccuracyに、kCLLocationAccuracyHundredMetersを設定します。これは、数百メートルの誤差くらいで位置情報を取得したい、という設定です。後で詳しく説明します。

delegateに自分を登録したので、下のように、ViewControllerがCLLocationDelegateを実装していることを追記しておきましょう。

class ViewController: UIViewController, CLLocationManagerDelegate {

位置情報取得の許可状態を確認する

viewDidAppearで、initLocationを呼び出し位置情報取得の準備をこの関数で行います。

    override func viewDidAppear(_ animated: Bool) {
       initLocation()
   }

initLocation()では以下のことをします。

    private func initLocation() {
       if !CLLocationManager.locationServicesEnabled() {
           print("No location service")
           return
       }

       switch CLLocationManager.authorizationStatus() {
       case .notDetermined:
           //ユーザーが位置情報の許可をまだしていないので、位置情報許可のダイアログを表示する
           locationManager.requestWhenInUseAuthorization()
       case .restricted, .denied:
           showPermissionAlert()
       case .authorizedAlways, .authorizedWhenInUse:
           if !didStartUpdatingLocation{
               didStartUpdatingLocation = true
               locationManager.startUpdatingLocation()
           }
       @unknown default:
           break
       }
   }

1. CLLocationManager.locationServicesEnabled()関数を呼び、位置情報サービスをサポートしていない端末(GPSを搭載していない端末)の場合はなにもしない

2. CLLocationManager.authorizationStatusの値を確認する。

authorizationStatusの状態によって以下の処理を行います。

.notDetermined (許可設定がまだされていないとき)

locationManager.requestWhenInUseAuthorization()を呼び出して、位置情報取得の許可をユーザーに聞く。こうすると、したのようにダイアログが表示されます。

画像8

.authorizedAlways, .authorizedWhenInUse:

このとき、ユーザーは位置情報取得を許可しているので、locationManager.startUpdatingLocation()を呼び出して、位置情報取得を始めます。

.restrictedまたは.denied (許可されていないとき)

showPermissionAlert()を表示し、ユーザーに設定アプリで位置情報の許可をオンにしてもらうようなダイアログを出します。

アラートの出し方は色々とあると思いますが、今回は、設定アプリへジャンプするボタンをもったUIAlertControllerを表示するようにします。

private func showPermissionAlert(){
       //位置情報が制限されている/拒否されている
       let alert = UIAlertController(title: "位置情報の取得", message: "設定アプリから位置情報の使用を許可して下さい。", preferredStyle: .alert)
       let goToSetting = UIAlertAction(title: "設定アプリを開く", style: .default) { _ in
           guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
               return
           }

           if UIApplication.shared.canOpenURL(settingsUrl) {
               UIApplication.shared.open(settingsUrl, completionHandler: nil)
           }
       }
       let cancelAction = UIAlertAction(title: NSLocalizedString("キャンセル", comment: ""), style: .cancel) { (_) in
           self.dismiss(animated: true, completion: nil)
       }
       alert.addAction(goToSetting)
       alert.addAction(cancelAction)
       self.present(alert, animated: true, completion: nil)
   }

スクリーンショット 2020-07-25 17.12.07

ユーザーが設定アプリを開くボタンをタップすると設定アプリに遷移します。

設定アプリではプライバシー > アプリ > 位置情報サービスから、位置情報設定を変える事ができます。

スクリーンショット 2020-07-25 17.19.47

この場所はなかなか探しにくいので、showPermissionAlertのなかでカスタムのViewControllerを表示し、この手順を説明したあとで設定アプリに飛ばすなどの工夫をするとUXの質を上げられます。実際にプロダクトをつくるときは、この辺も考慮して。

なお、位置情報以外にPush通知などの他のパーミッションの許可を取得するアプリは、設定画面のトップページの下にアプリが表示され、そこからも位置情報の許可の設定に行くことができます。この場合、上の4ステップではなく3ステップで設定を変えることができるため、位置情報以外のパーミッションを取得するアプリではこの3ステップでの変更方法をユーザーに説明してもいいかもしれません。

これでinitLocation()は完了です。もう一度同じコードを貼って起きます。

    private func initLocation() {
       if !CLLocationManager.locationServicesEnabled() {
           print("No location service")
           return
       }

       switch CLLocationManager.authorizationStatus() {
       case .notDetermined:
           //ユーザーが位置情報の許可をまだしていないので、位置情報許可のダイアログを表示する
           locationManager.requestWhenInUseAuthorization()
       case .restricted, .denied:
           showPermissionAlert()
       case .authorizedAlways, .authorizedWhenInUse:
           if !didStartUpdatingLocation{
               didStartUpdatingLocation = true
               locationManager.startUpdatingLocation()
           }
       @unknown default:
           break
       }
   }

位置情報許可設定の変更に反応する

ユーザーが位置情報を最初許可してくれなくて、後に設定アプリへ移動して許可をオンにした、という場合はその変更をアプリで取得する必要があります。

これを実装しておかないとせっかく上述のアラートビューから設定アプリに飛んでユーザーが位置情報取得を許可する設定に変更をしてくれても、ユーザーがアプリに戻ってきたときに何も起きない、ということになってしまいます。

ユーザーの許可設定の変更をイベントとして受け取るために、locationManagerのdidChangeAuthorizationを実装します。

ユーザーが設定で許可を与えてくれた場合、このメソッドが呼ばれます。

    func locationManager(_ manager: CLLocationManager,
                                 didChangeAuthorization status: CLAuthorizationStatus){
       if status == .authorizedWhenInUse {
           if !didStartUpdatingLocation{
               didStartUpdatingLocation = true
               locationManager.startUpdatingLocation()
           }
       } else if status == .restricted || status == .denied {
           showPermissionAlert()
       }
   }

ここで、statusが.authorizedWhenInUseに変わっていれば、locationManager.startUpdatingLocation()を呼び出して位置情報の取得を開始します。逆にそれ以外のステータスになってしまった場合はもう一度、先程のshowPermissionAlert()を表示して、設定アプリで設定してもらえるようお願いします。

以上でパーミッションの設定はおしまいです。

マップ表示や位置情報のとり方ではない部分にかなりの紙面を割いてしまいましたが、ユーザーが許可してくれないことにはアプリで必要な位置情報が以後全く取れなくなってしまうため、ここはUX的にも非常に注意深く設計する必要があります。

位置情報の取得

locationManager.startUpdatingLocation()で位置情報取得を始めると、GPSをつかってアプリが位置情報の取得を試みます。

GPSによる位置情報取得は非同期のプロセスなので、取得はdelegateメソッドを使って取得します。

以下の2つのメソッドをオーバライドします。

   func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
       if let location = locations.first {
           locationManager.stopUpdatingLocation()
           updateMap(currentLocation: location)
       }
   }

   func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
       print("Failed to find user's location: \(error.localizedDescription)")
   }

didUpdateLocationで位置情報取得の取得をします。今回は位置情報を1度だけ取得できればいいので、stopUpdatingLocation()をコールして位置情報取得をストップします。そして自作のupdateMap()関数に位置情報を渡します。

didFailWithErrorは、位置情報取得の失敗時に呼ばれる関数ですが、一応オーバーライドしてエラーの内容を表示するようにします。(私は、このエラーが呼ばれたところを見たことがないので、ひとまずprintするだけにしました。)

updateMapでは、ひとまず、取得した位置情報の緯度、経度を表示するだけにしておきます。

    // MARK: - Map
   private func updateMap(currentLocation: CLLocation){
       print("Location:\(currentLocation.coordinate.latitude), \(currentLocation.coordinate.longitude)")
   }

CLLocationクラスの、coordinateのなかに、latitude(緯度)とlongitude(経度)が入っていますので、これをprintで出力するようにしました。

アプリを実機で起動し下のような位置情報がデバッグコンソールに出力されれば成功です!

スクリーンショット 2020-07-26 11.05.18

シミュレーターを使ったテスト

実機を使わずに、シミュレーターを使ってテストすることができます。
シミュレーターのFeatures > Location > Custom Locationを選択し、

スクリーンショット 2020-07-25 11.56.01

出てきたCustom Locationのダイアログに緯度経度をいれます。

スクリーンショット 2020-07-25 11.56.43

これでも先程と同じようにコンソールに位置情報が表示されるのが確認できるのではないでしょうか?

スクリーンショット 2020-07-26 11.05.18

シミュレーターを使うと実機を毎回見なくていいということ以外にも意図した位置情報を入力して結果を見ることができるので便利です。例えばカフェの検索機能を作っているときに、カフェが密集したエリアにいた場合にどのように表示されるかを見て見たり、逆にカフェが一つもないエリアに行った時の状態をテストすることができます。

テストしたい場所の緯度経度はGoogle Mapを使って簡単に探すことができます。Google Mapで位置情報を知りたい場所を検索し、アドレスバーの途中、@マークの後に緯度、経度の情報が表示されていますのでこれを使ってみてください。

スクリーンショット 2020-07-25 11.52.20

まとめ

お疲れ様でした。第2回では、

- 位置情報取得の許可を得るダイアログの文言を設定する
- 位置情報取得の許可を得るダイアログを表示する
- ユーザーの許可が下りていないときにアラートをだして設定アプリに誘導する
- ユーザーの許可が下りたことをイベントで受け取って位置情報の取得を開始する
- 位置情報をCLLocationオブジェクトとして受け取り緯度、経度の値を取得する

ということをやりました。次回はこの取得した緯度経度の情報を使ってマップのフォーカスをユーザーの現在地に当てる方法を解説します。

このチュートリアルのソースコードは↓↓↓に置いてあります。
(GitHub上で☆をつけていただくと励みになります!)

https://github.com/mizutori/iOSMapStarterKit

ここまでのコードは、コミットハッシュ
24e7ee7f96c6171fea1ce924e45308e4786752be
でコミットしてあります。下のコマンドで、ロールバックして見てみてください。

git checkout 24e7ee7f96c6171fea1ce924e45308e4786752be

requestLocationとstartUpdatingLocation

iOSで位置情報を簡単に取得する方法として、startUpdatingLocationではなくて、requestLocationというAPIが紹介されています。これは1度だけ位置情報を取得する方法として便利なAPIですが、私はstartUpdatingLocationを使うようにしています。

requestLocationは位置情報の取得に10秒近く要することがあります。これは、requestLocationが裏でstartUpatingLocationを読んで位置情報を何回か取得し、そのなかから精度の良いものを選んでいるために時間がかかってしまうのです。

ユーザーに現在地をすぐに出したい場合や、近隣のカフェなどをぱっと検索して出したい場合、10秒ほどの待ち時間はUXを損ねる場合がほとんどなので私はstartUpdatingLocationを使うようにしています。

startUpdatingLocationはlocationManagerのdesiredAccuracy変数を使って、位置情報の精度を自由に変えることができます。

今回はkCLLocationAccuracyHundredMeters(半径数百メートルの誤差)を選択しています。

locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

この設定ですと、requestLocationよりもかなり早く、体感的にはほとんど場合1秒くらいで位置情報が取得できます。

位置情報の精度について

他にも下のように合計6つの選択しがあり、一番上のBestForNavigationが一番精度の高い設定、一番下のThreeKilometersが一番精度の低い設定になります。

public let kCLLocationAccuracyBestForNavigation: CLLocationAccuracy
public let kCLLocationAccuracyBest: CLLocationAccuracy
public let kCLLocationAccuracyNearestTenMeters: CLLocationAccuracy
public let kCLLocationAccuracyHundredMeters: CLLocationAccuracy
public let kCLLocationAccuracyKilometer: CLLocationAccuracy
public let kCLLocationAccuracyThreeKilometers: CLLocationAccuracy

精度を低くすればするほど早く位置情報を取得できるようになり、精度を高くすればするほど位置情報の取得に時間がかかると考えてください。

とりあえず自分の位置をマップに出す、近隣のカフェを検索する、その程度であれば今回のようなHundredMeters、あるいはKilometerのように求める精度を低くするだけで素早く位置情報を取得し、マップに表示できるようになります。自分にアプリのケースに最適な位置情報の精度設定を見つけてみてください。

このブログに関する質問やiOSアプリの開発の相談はこちらから↓↓↓

@mizutory
mizutori@goldrushcomputing.com

次回、第3回はマップの表示エリアやズームを変える方法を解説します↓↓↓









いいなと思ったら応援しよう!