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

前回の続き↓
[iOSアプリで位置情報を取得するときに配慮する点をまとめてみた①](https://qiita.com/reo0612/items/589ea45a0ce2cbd6ec07)

#はじめに
前回の記事を読んでない方に、簡単に説明するとiOSアプリで位置情報を取得するときに
配慮する点をまとめた内容となっています。

#前回までのあらすじ
前回、`端末の位置情報サービスの有無`のチェックは
アプリをインストール後の`初回起動のみ`しかされないと書きました。

初回起動時に位置情報サービスが`オフ`になっていたらこんなアラートを表示します↓
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/af1b2cde-9641-4633-e064-cfdf0d522a85.jpeg">

このチェックは`初回起動のみ`なので、2回目以降の起動時はアラートを表示するように改修しましたね↓
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/93344672-412f-95dd-c290-d9b7a6ba9794.jpeg">
今回は、`バックグラウンド状態`から`フォアグラウンド状態`にアプリを切り替えた時にも
`端末の位置情報サービスの有無`をチェックするようにしていきましょう。

#バックグラウンド状態から戻った時にチェックする(端末)
アプリが、`バックグラウンド状態`から`フォアグラウンド状態`に切り替わった時に
`端末の位置情報サービスの有無`をチェックするように処理するには

`AppDelegate`の[applicationWillEnterForeground(_ application: UIApplication)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground)にて行います。

※`AppDelegate`の呼ばれる順番は、こちらの記事が非常に分かりやすかったので載せておきます↓
[iOSアプリのライフサイクル](https://qiita.com/KenNagami/items/766d5f95940c76a8c3cd)

```swift:AppDelegate.swift
// アプリがフォアグラウンド状態に入ろうとしている時に呼ばれるメソッド
func applicationWillEnterForeground(_ application: UIApplication)
```

ですが、アラートを表示したいのでViewControllerのクラス内で処理を行いたいですね。

アプリの状態が変わったことを`AppDelegate`以外で感知して
ある処理を行いたい場合は、[NotificationCenter](https://developer.apple.com/documentation/foundation/notificationcenter)を使用します。

※こちらの記事を参考にして実装しました↓
[[Swift3.0] NotificationCenter を使ってアプリの状態に応じた処理を行う](https://joyplot.com/documents/2016/11/01/swift-notificationcenter-app-state/)

そして今回も、アラートの表示はこちらの方法で表示します↓
[UIAlertControllerをファイルを分けて実装してみる](https://qiita.com/reo0612/items/b1fa1e11b3792baa72b8)

```swift:ViewController
// 初回表示以外にもバックグラウンド復帰、タブ切り替えなどにも呼ばれるメソッド
// まだビューが表示されていないため、計算コストの高い処理は避ける
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // iOSからの通知(UIApplication.willEnterForegroundNotification)を受信して登録されたメソッド(willEnterForeground)を呼び出す
    NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

// 画面が非表示になる直前に呼ばれるメソッド
override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // 常に監視されているクラス(アプリで共通、UserDefaultと同じ感じ)なので呼び出したら毎回、削除(バグが起こりやすいため)

        // 例えば、NavigationControllerを使ってViewController1 > 2 > 3に画面遷移し、全てのViewControllerでNotificationCenterを呼び、willEnterForegroundNotificationのメソッドを書いていたとする
        // 画面遷移したとしても、ViewController 1,2,3は消えない(メモリー上に残っている)ので
        // ViewController3の画面内でバックグラウンド復帰した場合、他のViewController1,2も反応してしまうため毎回、削除しなくてはならない
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
}

// バックグラウンド状態から戻ってきた時に呼ばれるメソッド(自分で作る)
@objc private func willEnterForeground() {
    // バックグラウンド状態から戻ってきた時に端末の位置情報サービスがオフの場合
    if !CLLocationManager.locationServicesEnabled() {
         // アラート表示
         Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
   }
}
```

ビルドしてみると、`バックグラウンド状態`から戻った時にもアラートが表示されるようになりました↓
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/52894715-8134-9a8b-0e6b-7a1a4ece906f.gif)
これで、端末の位置情報サービスの有無を`起動時`と`バックグラウンド状態から戻った時`に
チェックするようになりましたね。

後、チェックしなければいけないのは`アプリの位置情報サービスの有無`です。

#アプリの位置情報サービスの有無をチェックする
位置情報を取得する際には、ユーザーに許可を貰わなければ取得出来ません。
なので、アプリはこのようなアラートを表示してユーザーに許可をリクエストします↓
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/8e4b4a91-a10b-cfcb-aed9-2fe313d899ee.jpeg">
ですが、もし、ユーザーが`許可しない`を選択したらどうでしょうか？
位置情報が必要なアプリの場合、許可を貰わなければ困りますよね。

なのでユーザーが、`許可しない`を選択した場合にアラートなどを表示したりして
`アプリの位置情報取得の許可`を促さなければいけません。

前回のソースコードを例に説明します。
※ちょっと長いので一部抜粋

```swift:ViewController
import UIKit
import CoreLocation

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
  }

extension 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:
            // アラートを表示して、アプリの位置情報サービスをオンにするように促す
            Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "")
            
        case .restricted:
            break
            
        default:
            break
        }
    }else {
       Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
    }
  }
}
```

`許可しない`を選択した場合、アラートがちゃんと表示されました。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/df2dcb46-8978-b891-7185-a10fcdc9cfd4.jpeg">

これでも、ちゃんとユーザーに対して`アプリの位置情報取得の許可`を促していますが
ここで、もう少し配慮してアラートの`OK`を選択したら`設定アプリ`に画面遷移するようにしましょう。

#設定アプリに画面遷移する
`設定アプリ`に画面遷移するメリットとして`アプリの位置情報取得の許可`がスムーズになり
ユーザーの手間が省かれることです。

画面遷移する方法は、こちらの記事が分かりやすかったので載せておきます↓
[Swift5を使ってURLスキームで設定画面に遷移する方法](https://qiita.com/kaneko77/items/7ab4ecb0a848c3c8264d)

この方法を用いて、先ほどのソースコードに追加していきます。

```swift:ViewController
import UIKit
import CoreLocation

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
  }

extension 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:
            // アラートを表示して、アプリの位置情報サービスをオンにするように促す
            // ユーザーに対して分かりやすようにmessageで、OKを選択すると設定アプリに画面遷移することを伝える
            Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
               // OKを選択した後、設定アプリに画面遷移する
               UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
            }
            
        case .restricted:
            break
            
        default:
            break
        }
    }else {
       Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
    }
  }
}
```

こちらをビルドしてみると、ちゃんと`設定アプリ`に画面遷移しました。
ですが、ここでも落とし穴があります。
![ezgif.com-gif-maker2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/ca3b305f-96b5-4b0f-0026-34358a50d1f1.gif)

この方法で、設定アプリに移動したのはいいけど
`アプリの位置情報取得の許可`をしないまま戻ったとしましょう。

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

この場面でも、アプリが`バックグラウンド状態`から戻った時に
`アプリの位置情報取得の有無`をチェックしなければいけません。

またまた記事が長くなりそうなので、今回はここら辺で終わります。
ここ間違っているよー！というのがありましたら、気軽にコメントして下さい。

最後まで読んで下さって、ありがとうございます！
ここまでのソースコードを下に載せておきます↓

#ソースコード
```swift:ViewController
import UIKit
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
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }
    
    @objc private func willEnterForeground() {
        if !CLLocationManager.locationServicesEnabled() {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
    }
}

extension 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:
            Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
            }
            
        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/0d151a493ed51f6920ae)