アプリバックグラウンド時にiBeaconレンジングを継続的に行う方法【iOS14.2】

#iBeaconの情報取得方式
- iOSの[CLLocationManager](https://developer.apple.com/documentation/corelocation/cllocationmanager)によるiBeaconの情報取得方法には**リージョン監視**と**レンジング**の2つの方式がある。両者を比較すると、後述のように**レンジングでしか実現できないユースケースがある**。
- リージョン監視はアプリがバックグラウンドでも動作するのに対し、レンジングはフォアグラウンドでのみ動作するとしている記事がよくみられる。しかし少なくとも後述の環境ではバックグラウンドでのレンジングが成功した。
- 本稿ではユーザによる画面オフやホーム画面への遷移によって**アプリがバックグラウンド状態になった後も継続してiBeaconのレンジングを行う方法**を記載する。

###リージョン監視
- IDで指定したビーコンの電波をiOS端末が検知できる範囲を「リージョン」と定義し、ユーザによるリージョンへの出入りやある時点でユーザがリージョン内外のどちらにいるのかを通知する。
- リージョンの内か外かでしか判定ができないため、**ユーザとビーコンとの距離の変化をモニタリングできない**。
- リージョン監視はアプリがフォアグラウンドでない場合も継続的に動作する（[iOSでBeaconの振る舞いを確認する](https://qiita.com/nosaka/items/5348dd5f8889d445d533)を参考にさせていただきました）。

###レンジング
- IDで指定したビーコンの電波強度等の情報を1秒ごとに取得する。
- レンジングを活用することで、ユーザとビーコンとのおよその距離に応じて処理を変えるなど、柔軟なアプリ設計が可能になる。
- 複数のビーコンを設置し、継続的に電波強度をモニタリングできれば、屋内移動ログの取得といったユースケースに応用できる。
- **アプリバックグラウンド時の継続的なレンジングは不可能とする情報が多いが、後述の方法で後述の方法で動作確認が取れた**。

#環境
- iPhone11 Pro（実機）
- iOS14.2
- Xcode Version12.2 

#バックグラウンドレンジングの実装
##準備
- アプリが位置情報を常に利用できるようにしたいため、Info.plistに`NSLocationAlwaysAndWhenInUseUsageDescription`を記載する。（[iOS13 & iOS12で位置情報を「常に許可」してもらうための導線設計に関するあれこれ](https://qiita.com/gaussbeam/items/96612baed85edb8f7c6d)を参考にさせていただきました）
- XcodeでTARGET > Signing & Capabilities > Background ModesのLocation updatesにチェックを入れる。
（[CoreLocationを使用してバックグラウンドモードで位置情報を取得する方法](https://program-life.com/191)を参考にさせていただきました）
- 端末側で、許可を促すアラートまたは設定からアプリに対する位置情報の利用を常に許可する。

##実装のポイント
リージョン監視及びレンジングの通常の実装に対し、下記3点の処理を追加する。
- `CLLocationManager`の`allowsBackgroundLocationUpdates`をtrueに設定し、**バックグラウンドでのロケーション更新を許可**する（①）。
- 同じく`CLLocationManager`の`pausesLocationUpdatesAutomatically`をfalseに設定し、**iOSによるロケーション更新の自動中断をオフ**にする（②）。
- レンジングを開始する前に`startUpdatingLocation()`で**ロケーションの更新を開始**する（③）。

##ソースコード
- 上記のポイント以外は通常のリージョン監視＋レンジングと同様である。CLLocationManagerのデリゲートメソッドの動きに関しては[iBeacon について](https://qiita.com/hs7/items/79d758394e838022e08c)をご参照ください。

```ViewController.swift
import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
    
    var myLocationManager:CLLocationManager!
    var myBeaconRegion:CLBeaconRegion!
    let UUIDList = [
        "12345678-1234-1234-1234-123456789012"
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myLocationManager = CLLocationManager()
        myLocationManager.delegate = self
        myLocationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        // バックグラウンドでのロケーション更新を許可しておく（①）
        myLocationManager.allowsBackgroundLocationUpdates = true
        // ロケーション更新の自動中断をオフにしておく（②）
        myLocationManager.pausesLocationUpdatesAutomatically = false
        
        let status = CLLocationManager.authorizationStatus()
        print("CLAuthorizedStatus: \(status.rawValue)");
        if(status == .notDetermined) {
            myLocationManager.requestAlwaysAuthorization()
        }
        // レンジングを始める前にロケーション更新を開始しておく（③）
        myLocationManager.startUpdatingLocation()
    }
    
    // リージョン監視を開始
    private func startMyMonitoring() {
        for i in 0 ..< UUIDList.count {
            let uuid: NSUUID! = NSUUID(uuidString: "\(UUIDList[i].lowercased())")
            let identifierStr: String = "Beacon:No.\(i)"
            myBeaconRegion = CLBeaconRegion(uuid: uuid as UUID, identifier: identifierStr)
            myBeaconRegion.notifyEntryStateOnDisplay = false
            myBeaconRegion.notifyOnEntry = true
            myBeaconRegion.notifyOnExit = true
            myLocationManager.startMonitoring(for: myBeaconRegion)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        print("didChangeAuthorizationStatus");
        switch (status) {
        case .notDetermined:
            print("not determined")
            break
        case .restricted:
            print("restricted")
            break
        case .denied:
            print("denied")
            break
        case .authorizedAlways:
            print("authorizedAlways")
            startMyMonitoring()
            break
        case .authorizedWhenInUse:
            print("authorizedWhenInUse")
            startMyMonitoring()
            break
        @unknown default:
            print("")
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        manager.requestState(for: region);
    }
    
    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        switch (state) {
        case .inside:
            print("iBeacon inside");
            manager.startRangingBeacons(in: region as! CLBeaconRegion)
            break;
        case .outside:
            print("iBeacon outside")
            manager.startRangingBeacons(in: region as! CLBeaconRegion)
            break;
        case .unknown:
            print("iBeacon unknown")
            break;
        }
    }
    
    // レンジング（ビーコンの電波強度等の情報を毎秒取得）
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
        if beacons.count > 0 {
            for beacon in beacons {
                print(beacon)
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("didEnterRegion: iBeacon found");
        manager.startRangingBeacons(in: region as! CLBeaconRegion)
    }
    
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("didExitRegion: iBeacon lost");
        manager.stopRangingBeacons(in: region as! CLBeaconRegion)
    }
}
```

##CLLocationManagerのバックグラウンド処理仕様
- 前述の準備と実装のポイントに従い、**バックグラウンドでのロケーション更新を開始することで、バックグラウンド時もレンジングが継続できるようになる**。
- ビーコン情報取得とロケーション取得はいずれもCLLocationManagerが担っているため、両者のバックグラウンド実行可否が一致する仕様になっていると考えられるが、公式ドキュメントの記載は見つけられなかった。

##アプリ実行時のXcodeコンソールログ
今回は下記の順でアプリの状態を変化させた。

- アプリ起動（フォアグラウンドへ移行）→画面オフでバックグラウンドへ移行→画面オンでフォアグラウンドへ移行→ホーム画面へ遷移させて再びバックグラウンドへ移行

アプリのライフサイクルを分かりやすくするためにAppDelegate内でライフサイクルイベントをprintしている。
(参考：[NotificationCenterを用いたライフサイクルイベントの検知](https://qiita.com/maebaru/items/2c4d5804f5979ecdd59d)）

- didFinishLaunch：アプリ起動
- willEnterForeground：フォアグラウンドへの移行
- didEnterBackground：バックグラウンドへの移行


```text:Xcodeコンソールログ
didFinishLaunch
CLAuthorizedStatus: 3
willEnterForeground
didChangeAuthorizationStatus
authorizedAlways
iBeacon inside
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.36m, rssi:-55, timestamp:2020-12-03 19:22:15 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.56m, rssi:-55, timestamp:2020-12-03 19:22:16 +0000)
didEnterBackground
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.81m, rssi:-61, timestamp:2020-12-03 19:22:17 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 1.04m, rssi:-63, timestamp:2020-12-03 19:22:18 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.76m, rssi:-54, timestamp:2020-12-03 19:22:19 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.84m, rssi:-59, timestamp:2020-12-03 19:22:20 +0000)
willEnterForeground
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.56m, rssi:-51, timestamp:2020-12-03 19:22:21 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.43m, rssi:-50, timestamp:2020-12-03 19:22:22 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.32m, rssi:-48, timestamp:2020-12-03 19:22:23 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.28m, rssi:-48, timestamp:2020-12-03 19:22:24 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.28m, rssi:-49, timestamp:2020-12-03 19:22:25 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.28m, rssi:-49, timestamp:2020-12-03 19:22:26 +0000)
didEnterBackground
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:2 +/- 0.28m, rssi:-49, timestamp:2020-12-03 19:22:27 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.26m, rssi:-48, timestamp:2020-12-03 19:22:28 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.25m, rssi:-48, timestamp:2020-12-03 19:22:29 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.25m, rssi:-48, timestamp:2020-12-03 19:22:30 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.25m, rssi:-48, timestamp:2020-12-03 19:22:31 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.25m, rssi:-48, timestamp:2020-12-03 19:22:32 +0000)
CLBeacon (uuid:12345678-1234-1234-1234-123456789012, major:0, minor:0, proximity:1 +/- 0.25m, rssi:-48, timestamp:2020-12-03 19:22:33 +0000)
```

バックグラウンド時も問題なくビーコン電波強度等を取得できている。
なお、今回は単一のビーコンでの20秒程度のレンジングログを示したが、3個のビーコンを対象とした数分単位のバックグラウンドレンジングも問題なく動作した。

#Appleのポリシーに適合する実装であるか
- **Appleがバックグラウンドでのビーコンレンジングをどの程度許容する姿勢であるかは不明**。直近でもiOS13でアプリのバックグラウンド実行が強制終了される事象が報告され、その後のアップデートで修正されていた（[iOS13.2.2公開、データ通信不具合、バックグラウンドアプリ強制終了など修正](https://iphone-mania.jp/news-265821/)）。今後のOSアップデートで仕様が変わる可能性がある。
- [WWDC19での発表](https://developer.apple.com/videos/play/wwdc2019/707/)を参照すると、**iOSでのバックグラウンド処理は必要性の高い特定のユースケースに限定して用いられるべきというAppleの意図**が読み取れる。また、バックグラウンド処理の実装に当たってはパフォーマンス・バッテリー消費・プライバシーの3点に留意すべきことも強調されている。バックグラウンドでのレンジングがこうしたAppleの意図にどこまで適合的かは不明確であるため、**ストア申請時は注意が必要である**。
- [App Store Reviewガイドライン](https://developer.apple.com/jp/app-store/review/guidelines/)では下記の記述がみられる。

>バックグラウンドでの位置情報取得モードを使用する場合は、それによってバッテリー持続時間が大幅に減少する可能性があることを通知してください

#補足
- 位置情報の利用を「常に許可」ではなく「Appの使用中のみ許可」としていた場合、バックグラウンド移行後のレンジングが約10秒で停止した。しかし、10秒より長い時間動作する現象もみられた。条件は不明。
- ロケーション更新時のデリゲートメソッド`locationManager(_:didUpdateLocations:)`を実装する必要はない。
- 今回は省略したが、必要性が無くなった時点でバックグラウンドでのレンジングを停止し、ユーザに見えにくい部分でのCPU負荷やバッテリー消費を避けるべきである。