[CLLocationManager]ジオフェンスを利用するための実装手順

# はじめに
ジオフェンスを利用したiOSアプリを開発する機会があったので、備忘録としてまとめたいと思います。

## そもそもジオフェンスとは？
仮想的な境界線(半径100mで設定したならば、半径100mの円)を引くことで、
その境界線の侵入離脱をトリガーに自分が行いたい処理を行うことができます。

例えば、

- 通知
- 他機器との接続

などが行えたりします。

通知であれば、ある領域内に入ったときに、
その領域付近のおすすめのお店を出したり、お店のチラシ情報を出したり使い道はいくらでもありそうです。
しかし、**登録できる領域の個数に制限がある**ので、その部分には配慮が必要です。

# 動作環境
iOS13.3
xcode11.5

# 実装手順
### 1. フレームワークの導入
位置情報やジオフェンスを利用するために、CoreLocationをimportします。

```swift
import CoreLocation
```

### 2. CLLocationManagerのインスタンス生成
メンバ変数として、CLLocationManagerとモニタリング領域に関するCLCircularRegionのインスタンスを作成します。

```swift
let locationManager: CLLocationManager = CLLocationManager()
var moniteringRegion: CLCircularRegion = CLCircularRegion()
```

### 3. 位置情報取得のユーザー許可設定
アプリで位置情報を利用するには、ユーザー認証を行わなければなりません。

```swift
// 位置情報取得のユーザー認証
self.locationManager.requestAlwaysAuthorization()
```
これに加えて、info.plistへ
**Privacy - Location When In Use Usage Description**
を設定する必要があり、表示する文言をValueに書くことができます。

### 4. デリゲートの設定

デリゲートを設定します。
この設定をしないと、後に記載するデリゲートメソッドが反応しなくなります。

```swift
// デリゲートの設定
self.locationManager.delegate = self
```

### 5. ジオフェンス領域の設定
ジオフェンス領域を設定したい中心地点の緯度・経度を設定し、半径を入力します。

```swift
// 中心位置の設定(緯度・経度)
let moniteringCordinate = CLLocationCoordinate2DMake(100.0, 100.0)
// モニタリング領域を作成
self.moniteringRegion = CLCircularRegion.init(center: moniteringCordinate, radius: 100.0, identifier: "name")
```

### 6. モニタリング開始・停止・状態取得
5で作成したモニタリング領域を利用することで、モニタリングの開始・停止・状態取得を行えます。

```swift
// モニタリング開始
self.locationManager.startMonitoring(for: self.moniteringRegion)

// モニタリング停止
self.locationManager.stopMonitoring(for: self.moniteringRegion)

// 現在の状態(領域内or領域外)を取得
self.locationManager.requestState(for: self.moniteringRegion)
```

### 7. 位置情報・ジオフェンスに関するデリゲートメソッド
以下は、デリゲートメソッドです。

```swift
extension ViewController: CLLocationManagerDelegate {
    
    // 位置情報の更新時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let location = locations.first!
        print("緯度：\(location.coordinate.latitude)、経度：\(location.coordinate.longitude)")
    }
    
    // 位置情報取得認可
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            print("ユーザー認証未選択")
            break
        case .restricted:
            print("位置情報サービス未許可")
            break
        case .denied:
            print("位置情報取得を拒否、もしくは本体設定で拒否")
            break
        case .authorizedAlways:
            print("アプリは常時、位置情報取得を許可")
            break
        case .authorizedWhenInUse:
            print("アプリ起動時のみ、位置情報取得を許可")
            break
        }
    }
    
    // モニタリング開始成功時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        print("モニタリング開始")
    }

    // モニタリングに失敗時に呼ばれる
    func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
        print("モニタリング失敗")
    }

    // ジオフェンス領域侵入時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("ジオフェンス侵入")
    }

    // ジオフェンス領域離脱時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("ジオフェンス離脱")
    }

    // ジオフェンスの情報が取得できないときに呼ばれる
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("モニタリングエラー")
    }

    // requestStateが呼ばれた時に呼ばれる
    public func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        if state == .inside {
            print("領域内です。")
        } else {
            print("領域外です。")
        }
    }
}
```

# まとめ
以上の手順で、実装を行うことでジオフェンスを手軽に利用することができます。

実際にジオフェンスを利用すると、位置情報の精度にもよりますが、
なかなかジオフェンス侵入・離脱が予期した位置で反応してくれない場合が多々あります。

特に、離脱に関しては、
最短でも半径+100m程度の距離で離脱のデリゲートメソッドが呼ばれているように感じました。

また、バックグラウンドでジオフェンスを利用する場合には、
他設定が必要になってくるので、また今後記事にしたいと思います。

# コード全体
```swift
import UIKit
import CoreLocation

class ViewController: UIViewController {
    
    let locationManager: CLLocationManager = CLLocationManager()
    var moniteringRegion: CLCircularRegion = CLCircularRegion()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 位置情報取得のユーザー認証
        self.locationManager.requestAlwaysAuthorization()
        
        // デリゲートの設定
        self.locationManager.delegate = self
        
        // 中心位置の設定(緯度・経度)
        let moniteringCordinate = CLLocationCoordinate2DMake(100.0, 100.0)
        // モニタリング領域を作成
        self.moniteringRegion = CLCircularRegion.init(center: moniteringCordinate, radius: 100.0, identifier: "name")
    }
    
    @IBAction func moniteringStartButton(_ sender: Any) {
        // モニタリング開始
        self.locationManager.startMonitoring(for: self.moniteringRegion)
    }
    
    @IBAction func moniteringStopButton(_ sender: Any) {
        // モニタリング停止
        self.locationManager.stopMonitoring(for: self.moniteringRegion)
    }
    
    @IBAction func requestStateButton(_ sender: Any) {
        // 現在の状態(領域内or領域外)を取得
        self.locationManager.requestState(for: self.moniteringRegion)
    }
    
}

extension ViewController: CLLocationManagerDelegate {
    // 位置情報の更新時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let location = locations.first!
        print("緯度：\(location.coordinate.latitude)、経度：\(location.coordinate.longitude)")
    }

    // 位置情報取得認可
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            print("ユーザー認証未選択")
            break
        case .restricted:
            print("位置情報サービス未許可")
            break
        case .denied:
            print("位置情報取得を拒否、もしくは本体設定で拒否")
            break
        case .authorizedAlways:
            print("アプリは常時、位置情報取得を許可")
            break
        case .authorizedWhenInUse:
            print("アプリ起動時のみ、位置情報取得を許可")
            break
        }
    }

    // モニタリング開始成功時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        print("モニタリング開始")
    }

    // モニタリングに失敗時に呼ばれる
    func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
        print("モニタリング失敗")
    }

    // ジオフェンス領域侵入時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("ジオフェンス侵入")
    }

    // ジオフェンス領域離脱時に呼ばれる
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("ジオフェンス離脱")
    }

    // ジオフェンスの情報が取得できないときに呼ばれる
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("モニタリングエラー")
    }
    
    // requestStateが呼ばれた時に呼ばれる
    public func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        if state == .inside {
            print("領域内です。")
        } else {
            print("領域外です。")
        }
    }
}
```