バックグラウンド時の位置情報常時取得にジオフェンスを駆使する手法を思いついたのでやってみた

# はじめに

これは [【その1】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1043) の3日目です。
2日目は [id:onk](http://blog.onk.ninja) さんによる [スライド共有サービス sharedoc を作りました](http://blog.onk.ninja/2015/12/02/release-sharedoc) です。

* [【その1】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1043)
* [【その2】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1044)

# 自己紹介

* HN : ogwmtnr
* ドリコム歴 8ヶ月目
 * 2015年4月入社
* 研究開発部 所属
 * 技術調査依頼をこなしたり、新しいデバイスやOS、FWのキャッチアップしたり
 * 完全新規プロジェクトのプロト作ったり
 * 周りの超人達に怯えながら精一杯がんばるマンな日々
* 古参音ゲーマー
 * 好きな era は era(nostal mix) 
* 豚バラ肉および豚バラ肉を用いた料理が好きです

# この記事について
最近位置情報の常時取得について調べる機会があり、iOS での動作を調査していく中で、バックグラウンド時における位置情報常時取得の基本的な方法である

1. 常に標準位置情報サービスを使い続ける
2. 大幅変更位置情報サービスに切り替える

とは別の、ジオフェンスを用いた**第3の方法**を思いついてやってみたので、その報告をしたいと思います。

# 通常のやり方

## 標準位置情報サービス

```objc
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.distanceFilter = 10.0;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
locationManager.delegate = self;
[locationManager startUpdatingLocation];
```

「バックグラウンドでも位置情報を使用する」をオンにして、ユーザーから位置情報「常に許可」のパーミッションを得る事に成功すれば、アプリがバックグラウンドに回っても位置情報の更新を受け続けられるようになります。
ただし精度の指定には慎重になる必要があり、iOS では以下の定数のうちいずれか1つを設定する必要があります。

* kCLLocationAccuracyBestForNavigation
* kCLLocationAccuracyBest
* kCLLocationAccuracyNearestTenMeters
* kCLLocationAccuracyHundredMeters
* kCLLocationAccuracyKilometer
* kCLLocationAccuracyThreeKilometers

設定された定数にしたがって、iOS システムが内部的に最良の取得方法を選択（GPS、GLONASS、Wifi、携帯基地局、Bluetooth）し、位置情報の更新を行います。
精度が高いほど消費電力は高いです。トレードオフですね。

Best の名称を冠する上位2つは積極的に GPS および GLONASS を使おうとするため、なかなかに電力を消費し、端末がホカホカします。
その代わり精度は折り紙つきなので「常に充電されている」ようなユースケースしか存在しない場合は、むしろ Best を積極的に採用すべきでしょう。
Uber や 日本交通 のアプリを載せて走ってる車は、おそらく充電しっぱなしで Best を使っているはずです。

`kCLLocationAccuracyHundredMeters`くらいならそこまで電力消費しないかなーと思い、バックグラウンドでも常時更新を行ってみたところ、止まっている時は問題無いのですが、移動を続けていると端末がホカりました。

試しに`kCLLocationAccuracyThreeKilometers`を設定して、弊社から目黒不動まで歩いて戻ってくる、というルートをたどり、取得した位置情報を線でつないだものが以下になります。
<img width="200" alt="IMG_0382.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/31212/13188a9f-9c90-338c-a352-806bd161dc15.jpeg">
ThreeKilometers とか書いてあるからすっごい誤差でるものなのかなと思ったけれどそんな事は無いです。
[こちら](http://stackoverflow.com/questions/32073119/location-is-updated-even-when-greater-gps-accuracy-is-set-kcllocationaccuracyth)にも書いてありますが、実際は最大誤差100m程度で収まるみたいです。
ただし、wifiや携帯基地局が豊富な都市だからこそ、とも言える結果ですので、一度計測してみるのをオススメします。

## バックグラウンド時は大幅変更位置情報サービスを使う

```objc
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
if ([CLLocationManager significantLocationChangeMonitoringAvailable]) {
    [locationManager startMonitoringSignificantLocationChanges];
}
```

[位置情報とマップ プログラミングガイド](https://developer.apple.com/jp/documentation/LocationAwarenessPG.pdf)中に、

> 大幅変更位置情報サービスでは、電力を大幅に節約しながらサービスを動かしたままにしておけます。
> これは、ユーザの位置変更を追跡する必要があるけれども、標準位置情報サービスで提供されるような高度な測地精度は必要としないアプリケーションに強く推奨されます。

とあり早速バックグラウンドで試してみたところ、
<img width="300" alt="drcm_to_shinagawa.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/31212/52a70dd3-af80-6a3e-b410-62ebac6e4982.jpeg">
弊社 -> 徒歩でJR目黒駅 -> 山手線でJR品川駅
の移動で **3回** 位置情報をゲットしました。
標準位置情報サービスに比べてかなり精度は下がるものの、サービスの要求レベルによっては十分かもしれません。
実際かなり省電力な体感で、端末もホカらず良いです。

# ジオフェンスを用いたやり方

## ジオフェンスとは
「特定範囲の仮想境界線」を示し、中心点と半径を設定して監視すると、「入った」「出た」タイミングでイベントを発行してくれます。
[こちら](http://qiita.com/HIkaruSato/items/5b1e7564c3b2d529f4c5)のページがわかりやすいです。
iOS 4 以降で使用可能、最大20個のジオフェンスを同時に監視できます。

## どうやっている？

フォアグラウンドでは標準位置情報サービスを使用している、という前提で説明。
今のところ、以下のやり方に落ち着いています。

1. アプリがバックグラウンドに回る
2. `stopUpdateLocation`を呼び出し、標準位置情報サービスを停止する
3. と同時に、最後に取得した位置情報を中心に、**複数のジオフェンス監視**を開始する
4. ジオフェンスのイベントが走ったら`startUpdateLocation`を叩く
5. 位置情報が更新されたら`3`に戻る
6. アプリがフォアグラウンドに戻るまで`3~5`を繰り返す

## なぜこうなったのか？

### 最初

1. アプリがバックグラウンドに回る
2. `stopUpdateLocation`を呼び出し、標準位置情報サービスを停止する
3. と同時に、最後に取得した位置情報を中心に**半径10~20m程度のジオフェンス監視**を開始する
4. ジオフェンスのイベントが走ったら`startUpdateLocation`を叩く

という形でした。
動いてない時はアプリが死んで、動き始めたらジオフェンスのイベントが発行されて、`startUpdateLocation`を叩いてバックグラウンドでの常時取得が再開するやったぜー！という狙いから。
しかしその狙いは外れてしまい、`4`の後は10数秒で更新が止まってしまいます。
[調べてみたところ](http://stackoverflow.com/questions/20187700/startupdatelocations-in-background-didupdatingtolocation-only-called-10-20-time)、バックグラウンドでも標準位置情報サービスを使い続けたいのであれば、**フォアグラウンドで`startUpdateLocation`を呼ぶ**必要がある、という事がわかりました。
つまりこの手法だと、Background Fetch でアプリが起こされた際の生存時間と同じだけしか、生き残っていられないという事です。

### 2回目

であるならば、

1. アプリがバックグラウンドに回る
2. `stopUpdateLocation`を呼び出し、標準位置情報サービスを停止する
3. と同時に、最後に取得した位置情報を中心に**半径10~20m程度のジオフェンス監視**を開始する
4. ジオフェンスのイベントが走ったら`startUpdateLocation`を叩く
5. 位置情報が更新されたら`3`に戻る
6. アプリがフォアグラウンドに戻るまで`3~5`を繰り返す

として、ジオフェンスから出入りする度に新しいジオフェンスを仕込みループ化する事で、10秒程度の生存時間でも常時取得しているかのような頻度での位置情報更新を期待しました。
しかしその狙いは外れてしまい、数回のループで止まってしまいます。

* `5`の時に取得できた位置情報の精度が悪すぎる（実際の位置と離れすぎ）
* 速い乗り物にライドしていて、`5`の位置情報の精度がそこそこ良くても、ジオフェンス監視開始時には既にジオフェンス外へ飛び出している

という仮説から、ループが上手く続かないのでは？と考えました。
そこから「移動距離や誤差に関わらず、ジオフェンスのイベントを拾うには、**数でカバーだ！**」という暴力的な発想に至り、最後の位置を中心に年輪のように20個のジオフェンスを仕込み、カバーするやり方にたどり着きました。

とてもわかりやすい図を描きましたので、こちらを見れば一目瞭然。
<img width="250" alt="yokuwakaru_zu.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/31212/cedf4124-be50-425b-f686-72d49b53ff30.jpeg">
黒点がジオフェンスの中心。
そこから距離の異なる複数のジオフェンスを配置。
黒点から離れていく赤点が自分の位置として、こうしておけばある程度のスピードで移動していてもジオフェンスが補足しやすくなる、という仕組み。

## 使い勝手どう？

大幅変更位置情報サービスよりは頻度高めに取れるな、といった感想です。
省電力っぷりもドッコイ。
11月中に実地テスト（出退勤チャレンジ）終わらせようと思い頑張ってみたのですが、まだ途中で止まったりする不具合あり、正解のコーディングに辿り着いていません。すいません。
（上手くいく時もあって、その時は素直に感動した。）

もし止まらないでちゃんと動き続ける実装を得たとしても、

* ジオフェンス管理周りでクライアントのコードが増える
* 20個全て専有すると、他の機能でジオフェンスを使いたくても使えない
 * 15個とか10個とか、減らしても体感変わらない可能性あるので、「絶対使えない」わけではないかも。

という点も見過ごせません。導入してみたいのであれば考慮する必要ありです。

## 改良できそう？

* Background Fetch をアプリに組み込んでおいて、発動したら`startUpdateLocation`を叩きつつジオフェンスを再度仕込む
* サイレントプッシュ通知を受け取ったら`startUpdateLocation`を叩きつつジオフェンスを再度仕込む

を組み合わせれば更に精度アップ＆生存率アップを見込めそうですが、更に管理大変になるのでベストではなさそう。

# まとめ

ジオフェンスを用いた方法を思いついてやってみました。
が、途中で取得が止まってしまう事があったりするので、仕様的に不可能なのか、改良の余地あるのか、まだ検証が必要そう。
今は素直に標準位置情報サービスまたは大幅変更位置情報サービスを使うのが良いと思います。
残念！

という感じで、新しいやり方を思いついたらさっさとコードにして実機で動かす。
失敗してもめげない。
良さげな仮説を思いつく限り、手を動かしていけば、きっと道は開ける。

# 明日は
[【その1】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1043) 4日目は [JunJunHiroi](http://qiita.com/JunJunHiroi) さんの [Ruby+Mechanizeによるファイル送信方法](http://qiita.com/JunJunHiroi/items/786b96576677487847ea) です。