目的
- iBeacon の Region 監視を利用して出退勤時のタイムカード切り忘れを解消するアプリを作成したい
- アプリは一度起動した後には立ち上げていなくてもOKな仕様としたい
問題点
-
locationManager(_:didExitRegion:)
検出が遅い- 領域外に出てから40秒〜1分程度検出に時間がかかる
- 打刻自体は社内WiFiからのみ可能な制限がある
- 職場からあまり離れる前に通知を行いたい
アイディア: 距離を測ってしまえばよいのでは?
-
locationManager(_:didRange:satisfying:)
で ビーコン の距離の変化を検出すればいけるのでは?-
didExitRegion
が発火する前にCLProximity.unknown
が発火していることは確認済み
-
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 region = 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」から立ち上がることができるのは以下の2タイミング1
- 明らかな移動があった場合
-
startSignificantLocationChanges()
で監視を開始 -
locationManager(_:didUpdateLocations:)
で値を取得
-
- 領域の出入りがあった場合
-
startMonitoring(for:)
で監視を開始 -
locationManager(_:didEnterRegion:)
,locationManager(_:didExitRegion:)
で値を取得
-
- 明らかな移動があった場合
- 立ち上がった後初めて距離測定が可能になる
- 今回の場合領域の出入りをするまで
CLProximity
による制御が行えない
- 今回の場合領域の出入りをするまで
引っかかった要因
- 「Background」状態では
locationManager(_:didRange:satisfying:)
でアプリを処理することが可能- そのため「Backgroundで距離を測定できる」という文献が大量に見つかり、「Suspended」「NotRunning」でも同様に動くものと勘違い
- デバッグ実行を停止した際の状態を「Background」と勘違い
- 実際には「Suspended」?
- アプリの要件としてはこの状態でも通知を正常に投げる必要がある
実現するとすれば...
- ビーコンを2台準備し「入口」「社内」の領域を作成する
- 「入口」の領域と「社内」の領域の didEnter をそれぞれ検知し、その順序によって入退室を検知する
- これなら「Not Running」状態でも即時にアプリが立ち上がり通知が出せるはず
備考
自分で検証した限りだと上記の通りになりましたが、確たる参考文献が見つからなかったため誤りのご指摘お待ちしています
参考
- startMonitoringSignificantLocationChanges
- startRangingBeacons
-
startMonitoringForRegion:desiredAccuracy:
- どれも deprecated のメソッドだが discussion の内容が最新のものより充実しているためこちらを参照
-
startMonitoringSignificantLocationChanges
とstartMonitoringForRegion
ではapplication(_:didFinishLaunchingWithOptions:)
に言及しているがstartRangingBeacons
では言及していないことがわかる
- アプリバックグラウンド時にiBeaconレンジングを継続的に行う方法【iOS14.2】
- Ranging beacons in background - is that possible?
- swift, detect ibeacons on the background and send notifications when in range
-
調べた限りだと「このイベントで発火する」という記述がそれぞれ存在するが、「この2つのみ」と断言する記述は見つからなかった。 検証した限りだと Ranging では立ち上がらない ↩