【iOS】Suspended状態のアプリからCoreLocation の 領域外検出を速やかに表示したかった（が挫折した）話

## 目的
- iBeacon の Region 監視を利用して出退勤時のタイムカード切り忘れを解消するアプリを作成したい
- アプリは一度起動した後には立ち上げていなくてもOKな仕様としたい

## 問題点
- **`locationManager(_:didExitRegion:)` 検出が遅い**
    - 領域外に出てから40秒〜1分程度検出に時間がかかる
    - 打刻自体は社内WiFiからのみ可能な制限がある
        - 職場からあまり離れる前に通知を行いたい

## アイディア: 距離を測ってしまえばよいのでは?

- `locationManager(_:didRange:satisfying:)` で ビーコン の距離の変化を検出すればいけるのでは？
    - `didExitRegion` が発火する前に `CLProximity.unknown` が発火していることは確認済み

```swift:CustomLocationManager.swift

import CoreLocation

import FirebaseAuth

// MARK: - CustomLocationManager

final class CustomLocationManager {

    static let shared = CustomLocationManager()

    func setup() {

        // CLLocationManager 関連の設定
        self.setupLocation()

        // ログイン中であれば監視開始
        if Auth.auth().currentUser != nil {
            
            self.start()
        }
        else {
            
            self.stop()
        }
    }

    func start() {

        guard let region = self.region else { return }

        // 位置情報の更新を開始する
        self.startUpdatingLocation()
        // Region の監視を開始する
        self.startMonitoring(for: region)
    }

    func stop() {

        guard let region = self.region else { return }
        
        // 位置情報の更新を終了する
        self.stopUpdatingLocation()
        // Region の監視を終了する
        self.stopMonitoring(for: region)
    }


    // MARK: - Private
    
    private func setupLocation() {

        self.delegate = self
        
        let region = self.createRegion()
        self.region = region
        
        // 精度は 100m オーダーで十分と判断
        self.desiredAccuracy = kCLLocationAccuracyHundredMeters
        // バックグラウンドでの位置情報更新を許可
        self.allowsBackgroundLocationUpdates = true
        // iOS によるロケーションの自動中断をオフにする
        self.pausesLocationUpdatesAutomatically = false
    }
    
    private func createRegion() -> CLBeaconRegion {
        
        guard let uuid = UUID(uuidString: "test-uuid-xxxxxxxxxxxxxxxx") else { fatalError() }
        
        let constraint = CLBeaconIdentityConstraint(uuid: uuid)
        
        let identifier = "test.region.com.xxxxxxxx"
        
        return CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: identifier)
    }
}


// MARK: - CLLocationManagerDelegate

extension CustomLocationManager: CLLocationManagerDelegate {
    
    // 一部省略
    
    // リージョンの距離観測結果を処理
    func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
        
        // ビーコンは1個のみとして実装
        if let ｒegion = self.region,
           let beacon = beacons.first(where: { $0.uuid == region.uuid }) {
            
            switch beacon.proximity {
            case .immediate, .near: CustomNotificationManager.shared.notify(identifier: "didEnter")
            case .unknown: CustomNotificationManager.shared.notify(identifier: "didExit")
            default: break
            }
        }
    }
}

```

## このアイディアの問題点
**「Not Running」「Suspended」状態だとリージョンとの距離変化でアプリが立ち上がらない**

## アプリが Not Running から立ち上がる条件
- CLLocationManagerManager 関連でアプリが「Not Running」から立ち上がることができるのは以下の２タイミング[^1]
    - 明らかな移動があった場合
        - `startSignificantLocationChanges()` で監視を開始
        - `locationManager(_:didUpdateLocations:)` で値を取得
    - 領域の出入りがあった場合
        - `startMonitoring(for:)` で監視を開始
        - `locationManager(_:didEnterRegion:)` , `locationManager(_:didExitRegion:)` で値を取得
- 立ち上がった後初めて距離測定が可能になる
    - **今回の場合領域の出入りをするまで `CLProximity` による制御が行えない**

[^1]: 調べた限りだと「このイベントで発火する」という記述がそれぞれ存在するが、「この２つのみ」と断言する記述は見つからなかった。 検証した限りだと Ranging では立ち上がらない

### 引っかかった要因
- 「Background」状態では `locationManager(_:didRange:satisfying:)` でアプリを処理することが可能
    - そのため「Backgroundで距離を測定できる」という文献が大量に見つかり、「Suspended」「NotRunning」でも同様に動くものと勘違い
- デバッグ実行を停止した際の状態を「Background」と勘違い
    - 実際には「Suspended」?
    - アプリの要件としてはこの状態でも通知を正常に投げる必要がある

## 実現するとすれば...
- ビーコンを２台準備し「入口」「社内」の領域を作成する
- 「入口」の領域と「社内」の領域の didEnter をそれぞれ検知し、その順序によって入退室を検知する
    - これなら「Not Running」状態でも即時にアプリが立ち上がり通知が出せるはず

## 備考
自分で検証した限りだと上記の通りになりましたが、確たる参考文献が見つからなかったため誤りのご指摘お待ちしています

## 参考
- [startMonitoringSignificantLocationChanges](https://developer.apple.com/documentation/corelocation/cllocationmanager/1423531-startmonitoringsignificantlocati)
- [startRangingBeacons](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620554-startrangingbeacons)
- [startMonitoringForRegion:desiredAccuracy:](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620560-startmonitoringforregion?language=occ)
    - どれも deprecated のメソッドだが discussion の内容が最新のものより充実しているためこちらを参照
    - `startMonitoringSignificantLocationChanges` と `startMonitoringForRegion` では `application(_:didFinishLaunchingWithOptions:) `に言及しているが `startRangingBeacons` では言及していないことがわかる
- [アプリバックグラウンド時にiBeaconレンジングを継続的に行う方法【iOS14.2】](https://qiita.com/yui_abe/items/8818a02a1ca6b6578c05)
- [Ranging beacons in background - is that possible?](https://forums.estimote.com/t/ranging-beacons-in-background-is-that-possible/151/18)
- [swift, detect ibeacons on the background and send notifications when in range
](https://stackoverflow.com/questions/52855671/swift-detect-ibeacons-on-the-background-and-send-notifications-when-in-range)