すれ違いを恋のきっかけにするアプリ「CROSS ME」の iOSクライアントを担当している松尾です。
CROSS ME は、街ですれ違った異性に「いいね」を送り、相手が「ありがとう」を返してくれることでマッチングし、メッセージを交換できるようになるアプリです。
すれ違いを判定するためにデバイスの位置情報をサーバに送信し、サーバ側の判定ロジックで処理しています。アプリが起動してない状態でも、場所を移動することで位置情報が送られるようになっています。今回はサーバ側のロジックは割愛させていただき、iOSクライアント側について書かせていただきます。
アプリの状態ごとの位置情報取得方法
まず、アプリの状態によって位置情報を取得する方法や精度が変わってきます。アプリの状態の種類については、下記でまとめてくださっていたので、参考にさせていただきます。
フォアグラウンド(Active / Inactive):
アプリが起動中。ユーザが操作できる状態です。この場合は通常の方法で位置情報が取得できるので特に問題無いと思います。
バックグラウンド(Background):
アプリがバックグラウンドモードで動作中。Info.plist でバックグラウンド用のパーミッション設定が必要です。UIBackgroundModes(Capabilities > Background Modes > Location updates)を使用することで、位置情報を取得することができます。
サスペンド(Suspended):
アプリはバックグラウンドで未実行。端末がメモリ不足になった場合iOSによって停止される場合がある。Backgroundと同じく、UIBackgroundModes を使用することで、位置情報が更新されたときにアプリがバックグラウンドモードに移行し、処理することが出来ます
停止(Not Running):
アプリが実行されていない。タスクからも切られた状態。この場合、 significant-change location service を利用することで、バックグラウンドでアプリが起動して位置情報を扱うことができます。
UIBackgroundModes で位置情報を取得する方法は色々な方が記事を書いているので、リンクだけ幾つか貼っておきます。
この記事ではアプリをタスクから切った状態で位置情報を取得する場合 (significant-change location service) について、少し詳しく見ていきたいと思います。
iPhoneシミュレータで動作確認するのが大変だったので、今回はそれについて書きたい!
significant-change location service を使ったサンプル
AppDelegate で起動時に startMonitoringSignificantLocationChanges() を呼び出します。
どこで呼んでも大丈夫ですが、アプリが Not Running の状態から起動した場合、継続して位置情報を受け取るためには再度 startMonitoringSignificantLocationChanges() を実行する必要があります。
精度は低いですが、アプリがどの状態でも位置情報を受け取ることが出来ますし、バッテリー消費もほとんどありません。
import UIKit
import CoreLocation
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
var window: UIWindow?
var locationManager = CLLocationManager()
// 位置情報更新時にアプリのプロセスが停止している場合もここから起動
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
log.writeToFile = true // ファイルに出力するロガーを使っています
log.debug?("==START==")
// 常に位置情報を取得する権限
locationManager.requestAlwaysAuthorization()
locationManager.delegate = self
// 位置情報起因で起動した場合
if launchOptions?[UIApplicationLaunchOptionsKey.location] != nil {
log.debug?("Launched with UIApplicationLaunchOptionsKey.location")
locationManager.startMonitoringSignificantLocationChanges()
}
return true
}
// バックグラウンドに入る時に呼ばれる
func applicationDidEnterBackground(_ application: UIApplication) {
log.debug?("<<DidEnterBackground>>")
if CLLocationManager.significantLocationChangeMonitoringAvailable() {
locationManager.startMonitoringSignificantLocationChanges()
}
}
// MARK: CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// ここで受け取った位置情報をアプリで使用します
log.debug?("<<DidUpdateLocation>>")
log.debug?(locations)
}
}
デバッグ方法
最終的には実機でテストしますが、初期開発時はシミュレータを使って挙動を確認しながら進めたいです。
significant-change location service で Not Running なアプリが起動する場合、
application:willFinishLaunchingWithOptions: と application:didFinishLaunchingWithOptions: が呼ばれ、 launchOptions に UIApplicationLaunchOptionsKey.location キーが入った状態で渡されます。
これの動作確認を iPhoneシミュレータで行いたかったのですが、一度うまく行ったと思って再度試してみても再現出来ないなど、挙動が安定せずに困りました。試行錯誤した結果、最終的に確実に成功できるパターンを見つけたのでここに記します。ベースは以下を参考にさせていただきましたが、そのままでは動作しなかったので手順を追加しています。
参考)
- シミュレータを起動して location 設定を None にします
- シミュレータを Reset Content and Settings.. で初期状態に戻します
- アプリをビルドして実行する
- Ctrl+H でシミュレータをバックグラウンドにする(Suspended 状態に移行)
- Ctrl+H, Ctrl+H でアプリをタスクから切る(Not Running 状態に移行)
- Edit Schemes で Launch の設定を Wait for executable to be launched に変更する
- アプリをビルドして実行する(Xcode がアプリの起動を待つ状態になる)
- シミュレータの location 設定を Freeway Drive にする
- アプリがバックグラウンドで起動して application:didFinishLaunchingWithOptions: が呼ばれる
少しそれますが、ログはファイルに吐いてアプリ内で表示するなどして、アプリがデバッガにつながっていない状態でも確認できるようにしておくと色々便利です。
実機テスト
電車に乗るか、ひたすら歩く。歩いた方が健康になれますが、おそらく電車の方がおすすめです。僕の場合、ログをファイルに出力しアプリ内のデバッグ画面で見れるようにしたので、散歩しながらたまに確認するなどしてテストしていました。
また、バッテリー消費について significant-change location service を使用した場合と、UIBackgroundModes を使用した場合で比べてみたところ、UIBackgroundModesは精度を最低まで落とした場合でも10倍ほどバッテリー使用量に差があると感じました(バッテリーがなくなりそうなところまで動作させた状態で「設定」アプリのバッテリー使用状況で見比べただけなので、精密な比較ではありませんが)。
まとめ
アプリがバックグラウンド(プロセスが生きている)状態の場合、UIBackgroundModes(Capabilities の Background Modes > Location updates) を使う。
アプリがタスクから切られていても位置情報を取得する場合は significant-change location service を使う。
いずれの場合でも、シミュレータで動作を確認することができます。
バッテリー消費を鑑みると、バックグラウンド状態の場合で位置情報が一時的に必要なだけなら UIBackgroundModes を使い、常に位置情報を取得し続けるようなアプリの場合は、精度の低さが許容できるなら significant-change location service を使った方が良いかと思います。