iOSアプリで位置情報を取得するときに配慮する点をまとめてみた①

今回は、iOSアプリで位置情報を取得するときに配慮しなくてはいけない点を自分なりにまとめてみようと思います。

#はじめに
私は今、現在地から飲食店を検索するアプリを開発しています。
※App Storeにリリースしているので良かったらダウンロードしてみて下さい！
[ふっぴーくん](https://apps.apple.com/jp/app/%E3%81%B5%E3%81%A3%E3%81%B4%E3%83%BC%E3%81%8F%E3%82%93/id1535141222)

自分の現在地から飲食店を検索するアプリなので、もちろん、現在地を取得しなくてはいけません。だったら、現在地を取得して「はい、終わり！」というシンプルなものではありませんでした。

まずは、現在地を取得するために最低限必要な手順から書いていきたいと思います。

#手順
iOSアプリで位置情報を取得するためには、

1.プライバシー情報取得の許可
2.CoreLocationフレームワークをインポート、CLLocationManagerクラスを初期化
3.位置情報サービスを使用するために、ユーザーに許可をリクエスト
4.CLLocationManagerDelegateプロトコルを採用し、デリゲートメソッドを実装

おおまかには、こんな感じの手順で位置情報が取得できます。
では、順を追って説明していきます。

#1.プライバシー情報取得の許可
iOSアプリでは、プライバシーに関わる情報を取得する際に許可が`必須`です。
位置情報もプライバシーに関わる情報なので、位置情報サービスを使用する前に許可を貰わないと使うことができません。

許可を得るには、info.plistにて使いますよ！という意思表示を示しましょう。
Key:`Location When In Use Usage Description`
Value:`任意の文字列(例：このアプリは位置情報サービスを使います。)`

私の場合はこんな感じです↓
![スクリーンショット 2021-01-08 11.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/8e70ea0c-780a-c146-c69c-d7ec68baaaef.png)


#2.フレームワークをインポート、クラスを初期化
まずは、位置情報関連のサービスがアプリ内で使えるように[CoreLocation](https://developer.apple.com/documentation/corelocation)フレームワークをインポートします。

``` swift:ViewController
import CoreLocation
```

そして、CoreLocationフレームワークに含まれている[CLLocationManager](https://developer.apple.com/documentation/corelocation/cllocationmanager)クラスを初期化します。
このクラスは、`位置情報の機能を管理`してくれています。

このクラスを、メンバプロパティ(クラス内で定義されるプロパティ)として
ViewControllerで宣言しインスタンスを初期化していきます。

``` swift:ViewController
import CoreLocation

final class ViewController: UIViewController {
 // CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
 var locationManager: CLLocationManager = {
  // インスタンスを初期化
  var locationManager = CLLocationManager()
  // 取得精度の設定(1km以内の精度)
  locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
  // 位置情報取得間隔(5m移動したら位置情報取得)
  locationManager.distanceFilter = 5
  return locationManager
 }()
}
```

CLLocationManagerクラスには、`位置情報取得の精度`や`位置情報取得の間隔`など色々な設定を自分で調節できます。
宣言と同時に書いてある[desiredAccuracy](https://developer.apple.com/documentation/corelocation/cllocationmanager/1423836-desiredaccuracy)・[distanceFilter](https://developer.apple.com/documentation/corelocation/cllocationmanager/1423500-distancefilter)プロパティの事ですね。

自分のアプリに合った設定をすることで、バッテリーが長持ちするというメリットがあります。
位置情報サービスはかなりの電力を使いますので、こういう所も配慮しなくてはいけませんね。

#3.ユーザーに許可をリクエスト
位置情報サービスを使用するときは、`ユーザーに対して許可をリクエスト`しなくてはいけません。
今回は、アプリの使用中に許可をリクエストしたいのでCLLocationManagerクラスのメソッド[requestWhenInUseAuthorization()](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620562-requestwheninuseauthorization)を使います。

```swift:ViewController
import CoreLocation

final class ViewController: UIViewController {
 // CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
 private var locationManager: CLLocationManager = {
  // インスタンスを初期化
  var locationManager = CLLocationManager()
  // 取得精度の設定(1km以内の精度)
  locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
  // 位置情報取得間隔(5m移動したら位置情報取得)
  locationManager.distanceFilter = 5
  return locationManager
 }()

 override func viewDidLoad() {
    super.viewDidLoad()
    // ユーザーに許可をリクエスト
    locationManager.requestWhenInUseAuthorization()
  }
}
```

ここで一旦、ビルドしてみたいと思います。

そもそも端末の位置情報サービスが`オフ`になっているとこんな感じのアラートが表示されます。
`設定`を選択することで設定アプリに画面遷移しますので、タップして位置情報サービスを`オン`にしてみましょう。

※ただし、アプリをインストール後の初回起動のみしか表示されません。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/af1b2cde-9641-4633-e064-cfdf0d522a85.jpeg">

位置情報サービスを`オン`にしてから再度、アプリを起動すると、こんな感じのアラートが表示されます。
このアラートは、`requestWhenInUseAuthorization()`を呼ぶと表示されます。

`1度だけ許可`や`Appの使用中は許可`などの選択肢がありますね。
これは`位置情報サービスの認証ステータス`でいくつかの種類があります。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/8e4b4a91-a10b-cfcb-aed9-2fe313d899ee.jpeg">

認証ステータスは、[CLAuthorizationStatus](https://developer.apple.com/documentation/corelocation/clauthorizationstatus)にまとめており、
アプリを初めて起動した時は`notDetermined`になっています。

なので現状、ビルドしたアプリは`notDetermined`状態という事ですね。

分かりやすいように認証ステータスをまとめてみました↓

|認証ステータス|意味|
|:---:|:---:|
|authorizedAlways|常に許可|
|authorizedWhenInUse|Appの使用中は許可(1度だけ許可も含まれる)|
|denied|許可しない|
|restricted|端末の位置情報サービスが許可されていない|
|notDetermined|位置情報サービスを使用できるかどうかを選択していない|

因みに、requestWhenInUseAuthorization( )は`notDetermined以外だと呼ばれない`です。
これはリクエストを許可しているのにアラートが表示されてしまうのを防ぐためだと思われます。

ユーザーにリクエストしたのはいいですが、まだ現時点では位置情報を取得することは出来ません。

#4.デリゲートメソッドの実装
[CLLocationManagerDelegate](https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate)プロトコルを準拠し、デリゲートメソッドを実装していきます。

```swift:ViewController
import CoreLocation

final class ViewController: UIViewController {
 //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
 private var locationManager: CLLocationManager = {
  // インスタンスを初期化
  var locationManager = CLLocationManager()
  // 取得精度の設定(1km以内の精度)
  locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
  // 位置情報取得間隔(5m移動したら位置情報取得)
  locationManager.distanceFilter = 5
  return locationManager
 }()

 override func viewDidLoad() {
    super.viewDidLoad()
    // ユーザーに許可をリクエスト
    locationManager.requestWhenInUseAuthorization()
    // デリゲート先を自分のViewControllerにする
    locationManager.delegate = self
  }
}

// ViewControllerにCLLocationManagerDelegateプロトコルを準拠
extention ViewController: CLLocationManagerDelegate {
    // CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        // アプリの現在の認証ステータス
        let status = manager.authorizationStatus
        
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            // 位置情報取得を開始
            manager.startUpdatingLocation()
            
        case .notDetermined:
            // ユーザーに許可をリクエスト
            manager.requestWhenInUseAuthorization()
            
        case .denied:
            break
            
        case .restricted:
            break
            
        default:
            break
        }
    }
  // 位置情報を取得・更新したら呼ばれるメソッド
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // 位置情報を取得する
        guard let gps = manager.location?.coordinate else {
            // 取得出来なかったらアラートを表示したりなど
            return
        }
        // 位置情報取得を停止
        manager.stopUpdatingLocation()
        //経度と緯度を出力する
        let lat = gps.latitude
        let lng = gps.longitude
        print("経度：\(String(describing: lat)), 緯度：\(String(describing: lng))")
    }
}
```

`requestWhenInUseAuthorization()`によって、認証ステータスが決定したら
[locationManagerDidChangeAuthorization](https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate/3563956-locationmanagerdidchangeauthoriz)デリゲートメソッドが呼ばれ、そのメソッド内で処理していきます。

とりあえずは、ユーザーが`.authorizedAlways`, `.authorizedWhenInUse`を選択した場合の処理と
`.notDetermined`を選択した場合の処理を記述しています。

位置情報を取得するためには、`startUpdatingLocation()`メソッドを呼ばなければいけません。
このメソッドにより、`位置情報取得を開始`してくれます。

そして、位置情報を取得したら[locationManager(_: didUpdateLocations:)](https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate/1423615-locationmanager)デリゲートメソッドが呼ばれ、
ここでやっと位置情報を取得することが出来ます。

では、長くなりましたが、ここからが本題となります。

#本題
題名にも書いてあるように、位置情報を取得する時には配慮しなくてはいけない点がいくつかあります。
まずは、`端末の位置情報サービスの有無`です。

端末の位置情報サービスがオフだった場合、「位置情報サービスをオンにして下さい」というアラートが
表示されると思いますが、アプリをインストール後の`初回起動のみ`しか表示されません。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/af1b2cde-9641-4633-e064-cfdf0d522a85.jpeg">
なので、2回目以降の起動時は端末の位置情報サービスが`オフ`だった場合、何も起こりません。
もちろん、位置情報を取得することも出来ません。

では、どうすれば良いのでしょうか？

#端末の位置情報サービスの有無をチェックする
自分で端末の位置情報サービスの有無をチェックするコードを記述しなくてはいけません。

しかし、そんなに難しい訳ではなく、CLLocationManagerクラスには便利なメソッドがあります。
それは、[locationServicesEnabled( )](https://developer.apple.com/documentation/corelocation/cllocationmanager/1423648-locationservicesenabled)です。

このメソッドは、端末で位置情報サービスが有効になっているかどうかを示すBool値を返します。
trueの場合は位置情報サービス`オン`、falseの場合は位置情報サービス`オフ`

では、このメソッドを先ほどのソースコードに組み込んでいきましょう。

そして、実際にアラートを表示したいので
私が過去にQiitaに投稿した記事のやり方でアラートを表示したいと思います↓
[UIAlertControllerをファイルを分けて実装してみる](https://qiita.com/reo0612/items/b1fa1e11b3792baa72b8)

```swift:ViewController
import CoreLocation

final class ViewController: UIViewController {
 //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
 private var locationManager: CLLocationManager = {
  // インスタンスを初期化
  var locationManager = CLLocationManager()
  // 取得精度の設定(1km以内の精度)
  locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
  // 位置情報取得間隔(5m移動したら位置情報取得)
  locationManager.distanceFilter = 5
  return locationManager
 }()

 override func viewDidLoad() {
    super.viewDidLoad()
    // ユーザーに許可をリクエスト
    locationManager.requestWhenInUseAuthorization()
    // デリゲート先を自分のViewControllerクラスにする
    locationManager.delegate = self
  }
}

// ViewControllerにCLLocationManagerDelegateプロトコルを準拠
extention ViewController: CLLocationManagerDelegate {
    // CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        // 端末の位置情報サービスがオンの場合
        if CLLocationManager.locationServicesEnabled() {
        // アプリの現在の認証ステータス
        let status = manager.authorizationStatus
        
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            // 位置情報取得を開始
            manager.startUpdatingLocation()
            
        case .notDetermined:
            // ユーザーに許可をリクエスト
            manager.requestWhenInUseAuthorization()
            
        case .denied:
            break
            
        case .restricted:
            break
            
        default:
            break
        }
    // 端末の位置情報サービスがオフの場合
    }else {
       Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
    }
 }
  // 位置情報を取得・更新したら呼ばれるメソッド
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // 位置情報を取得する
        guard let gps = manager.location?.coordinate else {
            // 取得出来なかったらアラートを表示したりなど
            return
        }
        // 位置情報取得を停止
        manager.stopUpdatingLocation()
        //経度と緯度を出力する
        let lat = gps.latitude
        let lng = gps.longitude
        print("経度：\(String(describing: lat)), 緯度：\(String(describing: lng))")
    }
}
```

`func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)`内で分岐させています。

位置情報サービスを`オフ`の状態でビルドしてみると、ちゃんと表示されました。
とにかく、これでアプリは`端末の位置情報サービスの有無`をチェックするようになりましたね。

これで一件落着！と思いますが、ここから更に想定して実装していかなければいけません。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/93344672-412f-95dd-c290-d9b7a6ba9794.jpeg">
例えば、ユーザーがアラートでOKを選択して位置情報サービスを`オフ`にしたままアプリに戻ったとしましょう。
どうなると思いますか？

流れとしてはこんな感じ↓
位置情報サービスオフ > アプリ起動 > アラート表示(OKを選択) > 設定アプリを開く > 何もしないまま戻る > ???

このままだと、アプリは`何もチェックしません`。

この解決方法はアプリが`バックグラウンド状態`から戻った時に`位置情報サービスの有無`をチェックすれば解決します。
では、どのようにするのかをまとめようと思いましたが、記事が長くなってしまったので後日、書こうと思います。

ここまで読んで下さって、ありがとうございます！
もし、ここは違うよ！というのがありましたら気軽にコメントして下さい。

ここまでのソースコードも載せておきます。

#ソースコード

```swift:ViewController
import CoreLocation

final class ViewController: UIViewController {
 private var locationManager: CLLocationManager = {
  var locationManager = CLLocationManager()
  locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
  locationManager.distanceFilter = 5
  return locationManager
 }()

 override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.requestWhenInUseAuthorization()
    locationManager.delegate = self
  }
}

extention ViewController: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if CLLocationManager.locationServicesEnabled() {
        let status = manager.authorizationStatus
        
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            manager.startUpdatingLocation()
            
        case .notDetermined:
            manager.requestWhenInUseAuthorization()
            
        case .denied:
            break
            
        case .restricted:
            break
            
        default:
            break
        }
    }else {
       Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
    }
 }

 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let gps = manager.location?.coordinate else {
            return
        }
        manager.stopUpdatingLocation()
        let lat = gps.latitude
        let lng = gps.longitude
        print("経度：\(String(describing: lat)), 緯度：\(String(describing: lng))")
    }
}
```

```swift:Alert
import UIKit

final class Alert {
    static func okAlert(vc: UIViewController,title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) {
        let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
        okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler))
        vc.present(okAlertVC, animated: true, completion: nil)
    }
}
```

#続き
[iOSアプリで位置情報を取得するときに配慮する点をまとめてみた②](https://qiita.com/reo0612/items/b25318e74a51b61548a0)